Linux多線程與同步

来源:http://www.cnblogs.com/maojianhui/archive/2016/02/02/5176774.html
-Advertisement-
Play Games

作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝! 典型的UNIX系統都支持一個進程創建多個線程(thread)。在Linux進程基礎中提到,Linux以進程為單位組織操作,Linux中的線程也都基於進程。儘管實現方式有異於其它的UN


作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!

 

典型的UNIX系統都支持一個進程創建多個線程(thread)。在Linux進程基礎中提到,Linux以進程為單位組織操作,Linux中的線程也都基於進程。儘管實現方式有異於其它的UNIX系統,但Linux的多線程在邏輯和使用上與真正的多線程並沒有差別。

 

多線程

我們先來看一下什麼是多線程。在Linux從程式到進程中,我們看到了一個程式在記憶體中的表示。這個程式的整個運行過程中,只有一個控制權的存在。當函數被調用的時候,該函數獲得控制權,成為激活(active)函數,然後運行該函數中的指令。與此同時,其它的函數處於離場狀態,並不運行。如下圖所示:

Linux從程式到進程

 

我們看到,各個方塊之間由箭頭連接。各個函數就像是連在一根線上一樣,電腦像一條流水線一樣執行各個函數中定義的操作。這樣的一個程式叫做單線程程式。


多線程就是允許一個進程記憶體在多個控制權,以便讓多個函數同時處於激活狀態,從而讓多個函數的操作同時運行。即使是單CPU的電腦,也可以通過不停地在不同線程的指令間切換,從而造成多線程同時運行的效果。如下圖所示,就是一個多線程的流程:

main()到func3()再到main()構成一個線程,此外func1()和func2()構成另外兩個線程。操作系統一般都有一些系統調用來讓你將一個函數運行成為一個新的線程。

 

回憶我們在Linux從程式到進程中提到的棧的功能和用途。一個棧,只有最下方的幀可被讀寫。相應的,也只有該幀對應的那個函數被激活,處於工作狀態。為了實現多線程,我們必須繞開棧的限制。為此,創建一個新的線程時,我們為這個線程建一個新的棧。每個棧對應一個線程。當某個棧執行到全部彈出時,對應線程完成任務,並收工。所以,多線程的進程在記憶體中有多個棧。多個棧之間以一定的空白區域隔開,以備棧的增長。每個線程可調用自己棧最下方的幀中的參數和變數,並與其它線程共用記憶體中的Text,heap和global data區域。對應上面的例子,我們的進程空間中需要有3個棧。

(要註意的是,對於多線程來說,由於同一個進程空間中存在多個棧,任何一個空白區域被填滿都會導致stack overflow的問題。)

 

併發

多線程相當於一個併發(concunrrency)系統。併發系統一般同時執行多個任務。如果多個任務可以共用資源,特別是同時寫入某個變數的時候,就需要解決同步的問題。比如說,我們有一個多線程火車售票系統,用全局變數i存儲剩餘的票數。多個線程不斷地賣票(i = i - 1),直到剩餘票數為0。所以每個都需要執行如下操作:

複製代碼
/*mu is a global mutex*/

while (1) {    /*infinite loop*/ if (i != 0) i = i -1 else { printf("no more tickets"); exit(); } }
複製代碼

如果只有一個線程執行上面的程式的時候(相當於一個視窗售票),則沒有問題。但如果多個線程都執行上面的程式(相當於多個視窗售票), 我們就會出現問題。我們會看到,其根本原因在於同時發生的各個線程都可以對i讀取和寫入。

我們這裡的if結構會給CPU兩個指令, 一個是判斷是否有剩餘的票(i != 0), 一個是賣票 (i = i -1)。某個線程會先判斷是否有票(比如說此時i為1),但兩個指令之間存在一個時間視窗,其它線程可能在此時間視窗內執行賣票操作(i = i -1),導致該線程賣票的條件不再成立。但該線程由於已經執行過了判斷指令,所以無從知道i發生了變化,所以繼續執行賣票指令,以至於賣出不存在的票 (i成為負數)。對於一個真實的售票系統來說,這將成為一個嚴重的錯誤 (售出了過多的票,火車爆滿)。

在併發情況下,指令執行的先後順序由內核決定。同一個線程內部,指令按照先後順序執行,但不同線程之間的指令很難說清除哪一個會先執行。如果運行的結果依賴於不同線程執行的先後的話,那麼就會造成競爭條件(race condition),在這樣的狀況下,電腦的結果很難預知。我們應該儘量避免競爭條件的形成。最常見的解決競爭條件的方法是將原先分離的兩個指令構成不可分隔的一個原子操作(atomic operation),而其它任務不能插入到原子操作中。

 

多線程同步

對於多線程程式來說,同步(synchronization)是指在一定的時間內只允許某一個線程訪問某個資源 。而在此時間內,不允許其它的線程訪問該資源。我們可以通過互斥鎖(mutex),條件變數(condition variable)和讀寫鎖(reader-writer lock)來同步資源。

 

1) 互斥鎖

互斥鎖是一個特殊的變數,它有鎖上(lock)和打開(unlock)兩個狀態。互斥鎖一般被設置成全局變數。打開的互斥鎖可以由某個線程獲得。一旦獲得,這個互斥鎖會鎖上,此後只有該線程有權打開。其它想要獲得互斥鎖的線程,會等待直到互斥鎖再次打開的時候。我們可以將互斥鎖想像成為一個只能容納一個人的洗手間,當某個人進入洗手間的時候,可以從裡面將洗手間鎖上。其它人只能在互斥鎖外面等待那個人出來,才能進去。在外面等候的人並沒有排隊,誰先看到洗手間空了,就可以首先衝進去。

上面的問題很容易使用互斥鎖的問題解決,每個線程的程式可以改為:

複製代碼
/*mu is a global mutex*/

while (1) { /*infinite loop*/ mutex_lock(mu);       /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/ if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu);     /*release mutex, make it unblocked*/ }
複製代碼

第一個執行mutex_lock()的線程會先獲得mu。其它想要獲得mu的線程必須等待,直到第一個線程執行到mutex_unlock()釋放mu,才可以獲得mu,並繼續執行線程。所以線程在mutex_lock()和mutex_unlock()之間的操作時,不會被其它線程影響,就構成了一個原子操作。

需要註意的時候,如果存在某個線程依然使用原先的程式 (即不嘗試獲得mu,而直接修改i),互斥鎖不能阻止該程式修改i,互斥鎖就失去了保護資源的意義。所以,互斥鎖機制需要程式員自己來寫出完善的程式來實現互斥鎖的功能。我們下麵講的其它機制也是如此。

 

2) 條件變數

條件變數是另一種常用的變數。它也常常被保存為全局變數,並和互斥鎖合作。

 

假設這樣一個狀況: 有100個工人,每人負責裝修一個房間。當有10個房間裝修完成的時候,老闆就通知相應的十個工人一起去喝啤酒。

我們如何實現呢?老闆讓工人在裝修好房間之後,去檢查已經裝修好的房間數。但多線程條件下,會有競爭條件的危險。也就是說,其他工人有可能會在該工人裝修好房子和檢查之間完成工作。採用下麵方式解決:

複製代碼
/*mu: global mutex, cond: global codition variable, num: global int*/
mutex_lock(mu) num = num + 1; /*worker build the room*/ if (num <= 10) { /*worker is within the first 10 to finish*/ cond_wait(mu, cond);     /*wait*/ printf("drink beer"); } else if (num = 11) { /*workder is the 11th to finish*/ cond_broadcast(mu, cond);        /*inform the other 9 to wake up*/ } mutex_unlock(mu);
複製代碼

上面使用了條件變數。條件變數除了要和互斥鎖配合之外,還需要和另一個全局變數配合(這裡的num, 也就是裝修好的房間數)。這個全局變數用來構成各個條件。

 

具體思路如下。我們讓工人在裝修好房間(num = num + 1)之後,去檢查已經裝修好的房間數( num < 10 )。由於mu被鎖上,所以不會有其他工人在此期間裝修房間(改變num的值)。如果該工人是前十個完成的人,那麼我們就調用cond_wait()函數。
cond_wait()做兩件事情,一個是釋放mu,從而讓別的工人可以建房。另一個是等待,直到cond的通知。這樣的話,符合條件的線程就開始等待。

當有通知(第十個房間已經修建好)到達的時候,condwait()會再次鎖上mu。線程的恢復運行,執行下一句prinft("drink beer") (喝啤酒!)。從這裡開始,直到mutex_unlock(),就構成了另一個互斥鎖結構。

那麼,前面十個調用cond_wait()的線程如何得到的通知呢?我們註意到elif if,即修建好第11個房間的人,負責調用cond_broadcast()。這個函數會給所有調用cond_wait()的線程放送通知,以便讓那些線程恢復運行。

 

條件變數特別適用於多個線程等待某個條件的發生。如果不使用條件變數,那麼每個線程就需要不斷嘗試獲得互斥鎖並檢查條件是否發生,這樣大大浪費了系統的資源。

 

3) 讀寫鎖

讀寫鎖與互斥鎖非常相似。r、RW lock有三種狀態: 共用讀取鎖(shared-read), 互斥寫入鎖(exclusive-write lock), 打開(unlock)。後兩種狀態與之前的互斥鎖兩種狀態完全相同。

一個unlock的RW lock可以被某個線程獲取R鎖或者W鎖。

如果被一個線程獲得R鎖,RW lock可以被其它線程繼續獲得R鎖,而不必等待該線程釋放R鎖。但是,如果此時有其它線程想要獲得W鎖,它必須等到所有持有共用讀取鎖的線程釋放掉各自的R鎖。

如果一個鎖被一個線程獲得W鎖,那麼其它線程,無論是想要獲取R鎖還是W鎖,都必須等待該線程釋放W鎖。

這樣,多個線程就可以同時讀取共用資源。而具有危險性的寫入操作則得到了互斥鎖的保護。

 

我們需要同步併發系統,這為程式員編程帶來了難度。但是多線程系統可以很好的解決許多IO瓶頸的問題。比如我們監聽網路埠。如果我們只有一個線程,那麼我們必須監聽,接收請求,處理,回覆,再監聽。如果我們使用多線程系統,則可以讓多個線程監聽。當我們的某個線程進行處理的時候,我們還可以有其他的線程繼續監聽,這樣,就大大提高了系統的利用率。在數據越來越大,伺服器讀寫操作越來越多的今天,這具有相當的意義。多線程還可以更有效地利用多CPU的環境。

(就像做飯一樣,不斷切換去處理不同的菜。)

 

本文中的程式採用偽C的寫法。不同的語言有不同的函數名(比如mutex_lock)。這裡關註的是邏輯上的概念,而不是具體的實現和語言規範。

 

總結

multiple threads, multiple stacks

race condition

mutex, condition variable, RW lock


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SQL Server 2008, 2008 R2, 2012 and 2014 完全支持TLS1.2加密傳輸 微軟高興地宣佈所有主流SQL Server客戶端驅動和SQL Server發行版已經支持Transport Layer Security 1.2簡稱TLS 1.2. 發佈時間是 2016年1
  • 自己的理解:用GO隔開,就相當於在不同的查詢視窗里執行SQL,GO需要單獨提交事務的 整理時參考博文: http://www.cnblogs.com/kissdodog/p/3163880.html http://lockrock.blog.51cto.com/2147255/775783 批處理是
  • 一、 概述 MySQL從3.23.15版本以後提供資料庫複製(replication)功能,利用該功能可以實現兩個資料庫同步、主從模式、互相備份模式的功能 二、 環境 操作系統:Linux 2.6.23.1-42.fc8 # SMP(不安裝XEN) Mysql版本:5.0.45-4.fc8 設備環境
  • logstash是一個強大的分析後端日誌的開源軟體,本文詳細介紹了logstash的配置和圖表使用,通過對訪問日誌的分析,能夠抓取用戶的地域分佈、使用的終端、感興趣的功能,介面的訪問時間排序,或者是異常檢測等等,而這些可以通過圖表的方式很直觀地展示出來,還可以進行關聯操作。
  • 內連接 左連接 右連接 全連接 交叉連接
  • 一、簡介 Linux的聲音系統或許是最無序的子系統部分!作為Server來說,聲音無足輕重,無人問津,而作為桌面來說太多的實現方案,各有各的長出和不足,ALSA經過多年的發展,基本統一了Linux音效卡硬體驅動層的藉口,OSS日漸退出,但是在ALSA之上的各個應用層面,方案和軟體之多讓人咋舌!ESD,...
  • 1.下載jdk.tar.gz文件 2.解壓jdk 命令:$sudo tar zxvf ./jdk.tar.gz 3.將解壓後的jdk放在/usr/lib/jvm下 4.查看本機是否還有jiava可選 命令:$sudo update-altematives--list java 如果沒有,下一步 5.
  • 問題描述: 我的 Arch Linux 已經用了快半年多,由於 Arch Linux 的滾掛問題,我從沒有直接升級過系統。軟體版本以及庫自然落後了一些。 就在我準備需要用到 NFS 時,掛載網路文件系統時由於 librpc 太舊而失敗了。所以看來我得更新 librpc 了。用 yaourt -Ss
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...