深入理解計算機系統第十二章學習筆記
應用級併發應用情況:訪問慢速I/O裝置;與人互動;通過推遲工作以降低延遲;服務多個網路客戶端;在多核機器上進行併發計算。
三種基本構造併發程式的方法:程序、I/O多路複用、執行緒
1、基於程序的併發程式設計
例如構建一個併發伺服器:
假設有1個伺服器和2個客戶端,伺服器正在監聽listenfd(3)上的連線請求,客戶端1向服務端請求,服務端返回一個連線描述符connfd(4),同時伺服器會生成一個子程序,子程序完全複製伺服器描述符表,並關閉其監聽符listenfd(3),伺服器關閉已連線的描述符connfd(4)。客戶端2請求時,同樣派生一個子程序服務。
優劣:
父子程序間共享檔案列表,但不共享使用者地址空間,一個程序不可能覆蓋另一個程序的虛擬記憶體。但缺點是獨立的地址空間使得程序共享狀態資訊更困難,需要IPC機制(管道,訊息佇列,共享記憶體,),比較緩慢。
2、基於I/O多路複用的併發程式設計
適用場景:1)網路客戶端發起連線請求 2)使用者在鍵盤上鍵入命令列
基本思想:用select函式將程序掛起,只有在一個或多個I/O事件發生後,才將控制返回給應用程式。
3、基於執行緒的併發程式設計
執行緒就是執行在程序上下文中的邏輯流。執行緒包括執行緒ID、棧、棧指標、程式計數器、通用目的暫存器和條件碼。
執行緒執行模式:先有一個主執行緒,主執行緒建立一個對等執行緒,兩個執行緒併發執行。
多執行緒和多程序的區別:1、一個執行緒的上下文要比一個程序的上下文小很多,切換更快。2、執行緒不是按照嚴格的父子層次來組織的,對等執行緒池的概念,一個執行緒可以殺死它的任何對等執行緒。
POSIX執行緒(pthreads):C程式處理執行緒的一個標準介面。
1)建立執行緒:pthread_create
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);
tid存執行緒的ID;attr引數可以改變執行緒的預設屬性;f是新執行緒上下文中執行的執行緒例程;arg表示輸入變數
2)終止執行緒:
四種方式:當頂層的執行緒例程(函式)返回時,執行緒會隱式地終止;通過pthread_exit()來顯示終止;呼叫exit()函式,會終止程序以及所有與該程序相關的執行緒;另一個執行緒通過當前執行緒的ID並呼叫pthread_cancel(pthread_t tid)來終止執行緒。
3)回收已終止執行緒的資源:
用pthread_join(pthread_t tid,void **thread_return)來等待一個執行緒終止,將執行緒返回的void* 指標賦值給thread_return指向的位置,並回收已終止執行緒佔用的記憶體資源。
4)分離執行緒:
執行緒的兩種狀態:結合態(可以被其他執行緒回收和殺死)和分離態(記憶體空間由系統自動釋放)
預設執行緒是結合態,為了避免記憶體洩漏,可以通過pthread_detach(pthread_t tid)分離,執行緒可以通過pthread_detach(pthread_self())來自我分離。
5)初始化執行緒:
pthread_once(ptread_once_t *once_control,void (*init_routine)(void))函式允許你初始化與執行緒例程相關的狀態。pthread_once_t once_control=PTHREAD_ONCE_INIT是一個全域性變數,第一次用once_control為引數呼叫pthread_once時,將呼叫init_routine來初始化。
6)多執行緒程式中的共享變數
執行緒記憶體模型:執行緒具有獨立的上下文(包括執行緒ID、棧、棧指標、程式計數器、條件碼、通用目的暫存器值),同一程序的不同執行緒共享進行上下文的剩餘部分(包括程式碼、讀寫資料、堆)。
將變數對映到記憶體:
全域性變數:在執行時,虛擬記憶體只包含每個全域性變數的一個例項,任何執行緒都可以引用。
本地自動變數:定義在函式內部但沒有static 屬性的變數,存在於每個執行緒的棧中。
本地靜態變數:定義在函式內部有static屬性的變數,在虛擬記憶體裡。
7)用訊號量同步執行緒
(a)用共享變數的方法會引入同步錯誤,因此提出訊號量的概念.訊號量s是具有非負整數的全域性變數,有兩種特殊的操作:
P(s):如果s非零,則將s減1,並立即返回。如果s為0,則掛起當前執行緒,直到s變為非0。
V(s):將s加1。如果有執行緒阻塞在P操作等待s變為非0,V操作會重啟這些執行緒中的一個,然後該執行緒會將s減1,但重啟的順序沒有定義。
P和V的操作不可分割,確保一個正在執行的程式不會進入一種狀態,就是一個正確初始化的訊號量由負值。稱為訊號量不變性。
POSIX中的函式:int sem_init(sem_t *sem,0,unsigned int value)初始化訊號量sem為value。int wait_init(sem_t *s)對應於P(s)。int sem_post(sem_t *s)對應於V(s)。
(b)使用訊號量來實現互斥
將每個共享變數和一個訊號量s(初始值為1)聯絡起來,然後用P(s)和V(s)操作將相應的臨界區包圍起來,這種方式的訊號量叫二元訊號量。以提供互斥為目的的二元訊號量稱為互斥鎖。在一個互斥鎖上執行P操作稱為對互斥鎖加鎖。執行V操作稱為對互斥鎖解鎖。
例如:
先宣告一個訊號量mutex:
volatile long cnt = 0;sem_t mutex;
主例程將mutex初始化為1:
sem_init(&mutex,0,1);
線上程中對共享變數cnt的更新包圍P和V操作
for(i = 0;i<niters;i++){
P(&mutex);cnt++;V(&mutex);
}
(c)利用訊號量來排程共享資源
一個執行緒用訊號量操作來同質另一個執行緒,程式狀態中某個條件為真。
生產者-消費者問題:生產者執行緒—>有限的緩衝區—>消費者執行緒
需要3個訊號量:mutex訊號量提供互斥的緩衝區訪問,slots和items訊號量分別記錄空槽位和可用專案的數量。
讀者-寫者問題:
一組併發的執行緒要訪問一個共享物件,寫者必須擁有對物件的獨佔的訪問,讀者可以和無限多個其他讀者共享物件。
8)其他併發問題
執行緒安全:當多個併發執行緒反覆呼叫一個函式,該函式可以得到正確的結果,則該函式是執行緒安全的。
四類執行緒不安全的函式:不保護共享變數的函式;保持跨越多個呼叫的狀態的函式;返回指向靜態變數的指標的函式(通過加鎖-複製解決);呼叫執行緒不安全函式的函式。
9)競爭
當一個程式的正確性依賴於一個執行緒要在另一個執行緒到達y點之前到達它的控制流中的x點是,就會發生競爭。通過動態的為每個引數分配一個獨立的塊,並傳遞給執行緒例程一個指向這個塊的指標。
10)死鎖
指的是一組執行緒被阻塞,等待一個永遠不會為真的條件。程式死鎖是因為每個執行緒都在等待其他執行緒執行一個不可能發生的V操作。
解決死鎖的方法:互斥鎖加鎖順序規則(給定所有互斥操作的一個全序,如果每個執行緒都以一種順序會的互斥鎖並以相反的順序釋放,那麼這個程式就是無死鎖的)