前言:
這是該書的最後一章了,從彙編和并行的角度上,徹底解釋清楚了兩個線程對同一個全局變量執行加一出錯的原因。并且引入了進度圖的概念,解釋清楚死鎖産生的原因。
我的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),給所有的鎖配置設定一個全序,每個線程按照這個順序來請求鎖,并且按照逆序來釋放。這樣這個程式就是無死鎖的。
總結:
這一部分對于并發程式設計的解釋,徹底說清了并發程式設計的原理,會遇上的問題,以及解決的辦法。以後遇上多線程時再也不是問題了。