天天看點

《CSAPP》并發程式設計

前言:

     這是該書的最後一章了,從彙編和并行的角度上,徹底解釋清楚了兩個線程對同一個全局變量執行加一出錯的原因。并且引入了進度圖的概念,解釋清楚死鎖産生的原因。

我的github:

我實作的代碼全部貼在我的github中,歡迎大家去參觀。

https://github.com/YinWenAtBIT

第十二章:并發程式設計

定義:

如果邏輯控制流在時間上重疊,那麼他們就是并發的。

一、并發程式設計的用處:

1.通路慢速的I/O裝置:

等待慢速的IO裝置時,可以讓CPU轉去處理其他的指令。

2.與人互動:

每次使用者請求某種操作時,一個獨立的邏輯并發流創造出來執行這個操作

3.通過推遲工作來降低延遲:

4. 服務多個網絡用戶端:

5.在多核機器上進行并行計算:

6. 程序

7. IO多路複用

8. 線程

二、基于程序的并發程式設計:

三、基于IO多路複用的并發程式設計:

這兩個部分已經在《UNIX網絡程式設計》中已經有了詳細的叙述了,是以略過。

線程:

一、基于線程的特點:

1. 線程就是運作在程序上下文中的邏輯流:

線程由核心自動排程運作

2. 每個線程都有自己的線程上下文:

唯一的整數線程ID,棧,棧指針,程式電腦,通用目的計數器和條件碼。

3. 所有運作在一個程序裡的線程都共享該程序的整個虛拟位址空間

二、線程執行模型:

1.程序開始時那個線程稱為主線程:

主線程建立的其他線程都是對等線程

2. 和一個程序相關的線程組成一個對等的線程池:

主線程和其他線程的差別僅僅在于它是第一個運作的線程

3. 一個線程可以殺死任何的對等線程,或者等待它的任意對等線程終止

4. 每個對等線程可以讀寫相同的共享資料

三、線程函數:

1. 建立線程,傳回線程ID:

int pthread_create

pthread_t pthread_self

2. 終止線程:

void pthread_exit(void * thread_return)

當頂層的線程例程傳回時,線程會隐式的終止

通過調用pthread_exit函數,線程會顯式的終止。

如果主線程調用pthread_ext,它會等待其他對等線程終止,然後再終止主線程和整個程序。

某個對等線程調用UNIX的exit函數,該函數終止程序以及所有該程序的線程

另一個對等線程通過以目前線程ID為參數調用pthread_cancle懶終止目前線程

3. 收回已終止的檢測的資源:

iint pthread_join(pthread_t tid, void ** thread_return)

這個函數會組設,直到線程tid終止。然後釋放線程占用的資源

4. 分離線程:

pint pthread_detach(pthread_t tid)

線程時可結合的或者是可分離的,一個可結合的線程可以被其他線程收回資源或者殺死。

一個分離的線程時不可以被其他線程收回或者殺死的。它的存儲器資源線上程終止時由系統自動釋放。

四、線程存儲器模型:

1.每個線程都有自己獨立的線程上下文:

線程ID,棧,棧指針,程式計數器,條件碼和通用目的寄存器

2. 每個線程與其他線程一起共享程序上下文的剩餘部分:

整個使用者的虛拟位址空間,由隻讀代碼段,讀寫資料區,堆,共享庫代碼和資料區域組成。共享已經打開的檔案集合。

3. 寄存器是不共享的,虛拟存儲器總是共享的。

五、變量映射到儲存器:

1. 全局變量:

運作時,虛拟儲存器的讀寫區域包含每個全局變量的一個執行個體,任何線程都可以通路

2. 本地自動變量:

每個線程都包含它自己所有的本地變量的執行個體

3. 本地靜态變量:

定義在函數内部并且有static屬性的變量,在虛拟存儲器的讀寫區域有每個靜态變量的一個執行個體,每個線程都可以通路。

信号量同步線程:

一、對共享變量調用的方式:

在彙編中,每個線程都從儲存器中取出變量,然後加載到自己的寄存器上,然後對變量進行操作,操作完之後再寫回儲存器中。

這樣在幾個線程同時對共享變量操作的時候,如果一個線程剛剛更新完了共享變量,還沒來得及寫回去,另一個線程就從儲存器中取出了變量,将會導緻同步錯誤。

一般而言,沒辦法預測作業系統是否會為線程選擇一個正确的順序,是以不能依賴這樣的順序來編寫程式。

二、信号量:

可以用信号量來實作互斥,這一部分在APUE中有詳細的講訴,在這裡就不細說。

三、生産者消費者問題:

需要使用1個互斥量來控制對資源的通路,一個信号量代表槽,一個信号量代表産品。

四、讀寫者問題:

有兩種模式,一種是讀者優先,這樣情形使用一個互斥鎖,加上讀者計數器就可以完成。

第二種寫着優先模式:

需要使用多個互斥量和計數,來完成這個複雜的邏輯關系。

五、基于預線程化的并發伺服器:

這個問題使用生産者消費者模型可以解決。将每個收到的connfd當做生産出來的資料加入即可。

其他并發問題:

一、線程安全:

1. 定義:

一個函數被稱為線程安全你的,僅當被多個線程反複調用時,它會一直産生正确的結果

二、線程不安全種類:

1. 不保護共享變量的函數:

這個就是沒有對共享變量進行加鎖,修改的方式隻要加上互斥量即可。改完之後速度會因為同步導緻變慢

2. 保持跨越多個調用狀态的函數:

這類函數中有一個static變量,用來儲存上一次的結果,比如rand函數。因為這一次調用的結果依賴于上一次調用的結果,那麼将導緻被多個線程調用的時候結果不唯一。

修正這類函數的唯一辦法就是重寫它,放棄使用static資料,而是使用調用者傳遞來的狀态資訊。這樣做的缺點就是已經在使用這個函數的其他代碼需要修改。

3. 傳回指向靜态變量的指針的函數:

結果使用static來儲存結果,然後傳回一直指向這個結果的指針,這樣就會引入競争問題。

解決辦法:

a. 重寫函數,讓調用者傳遞存放結果的位址,這樣的壞處就是使用了這個函數的代碼需要重寫。

b. 對這個線程不安全的函數加鎖,直到結果被拷貝出去之後再解鎖,但是實際上也還是得修改源代碼。

4. 調用線程不安全函數的函數:

這種情況就是函數 F 調用了線程不安全的函數 G,

如果G是第二類函數,那麼F還是不安全的

如果G是1,3 類函數,可以在F中對G的調用加鎖,就可以是的F變成線程安全你的函數了。

三、可重入性:

定義:

可重入的特别在于,當一個可重入函數被多個線程調用時,不用引用任何共享資料。

可重入函數是線程安全函數的一個真子集。

四、死鎖:

1. 定義:

死鎖是一組線程被阻塞了,等待一個永遠不會為真的條件。使用進度圖可以非常好的了解死鎖發生于避免。

2. 互斥鎖加鎖規則順序:

如果對于程式中每對互斥鎖(s,t),給所有的鎖配置設定一個全序,每個線程按照這個順序來請求鎖,并且按照逆序來釋放。這樣這個程式就是無死鎖的。

總結:

這一部分對于并發程式設計的解釋,徹底說清了并發程式設計的原理,會遇上的問題,以及解決的辦法。以後遇上多線程時再也不是問題了。

繼續閱讀