三種基本的構造併發程序的方法:進程,I/O多路複用,線程
1、基於進程的併發編程
- 每個邏輯控制流都是一個進程,由內核來調度和維護;有獨立的虛擬地址空間;進程間通信採用顯式的IPC機制
- 父子進程之間共享文件表但是不共享用戶地址空間。他的好處就是因爲是有獨立的地址空間,所以不會互相不小心覆蓋內存,但是壞處也很明顯,這樣會使進程間共享狀態信息變得困難,要使用進程間通信(IPC)機制,而且因爲進程控制和IPC的開銷很高,所以會比較慢。
- IPC:管道,FIFO,系統V共享內存,系統V信號量
2、基於I/O多路複用的併發編程
- selcet函數會一直阻塞,直到集合中有一個描述符準備好可以讀
- 事件驅動編程,相對於基於進程的設計而言,他的優點是讓程序員對程序行爲有更好的控制;每個邏輯流能共享全部地址空間,共享數據更容易;便於調試;不需要進程間的切換調度,更加高效。現代高性能服務器的選擇
- 缺點就是編碼複雜,阻塞處理,不能充分利用多核處理器
3、基於線程的併發編程
- 每個線程有唯一的線程ID、棧、棧指針、程序計數器、通用目的寄存器、條件碼
- 和進程一樣,內核自動調度,通過ID;
- 和I/O多路複用一樣,運行在單一進程的的上下文中,共享進程的虛擬地址空間和所有內容,包括代碼、數據、堆、共享庫和打開的文件;
- 不同於父子進程之間,一個進程相關的線程組成的是對等的線程池,一個線程可以殺死任一對等線程,也可以等待任意對等線程的終止;對等線程可以讀寫相同的共享數據;
4、共享變量
- 變量的內存模型,如上第一條和第三條
- 各自獨立的線程棧通常是被相應的線程獨立訪問的,但也不一定,如果一個線程通過某種方式得到一個指向其他線程棧的指針,就可以讀寫這個棧的任何部分,這是因爲不同的線程棧是不對其他線程設防的;
5、用信號量進行線程同步
- P操作
- s非零,P操作將s-1,返回;s==0,線程掛起,直到s!=0,V操作重啓線程;重啓後P操作s-1,返回;
- V操作
- s+1;有線程阻塞等待s!=0,V操作重啓線程,s-1完成P操作。
- 使用信號量來實現互斥——互斥鎖
- 二元信號量(s=0或1)
- P操作加鎖
- V操作解鎖
- 使用信號量來調度共享資源
- 一個線程用信號量操作來通知另一個線程
- 生產者—消費者問題
- 生產者
- 消費者
- 條件變量
- 讀者—寫者問題——讀寫鎖
- 這種方法可能會導致飢餓,即一個線程無限期阻塞,這是因爲在V操作無法控制他重啓的是哪一個等待線程
- 基於預線程化的併發服務器
- 類似於生產者和消費者問題,主線程創建一組工作者線程,然後進入無限循環將已連接描述符放到緩衝區SBUF中,生產者線程等待直到有已連接的描述符需要處理
- 這是一個事件驅動服務器,帶有主線程和工作者線程的簡單狀態機
6、使用線程提高並行性
- 並行程序是運行在多個處理器上的併發程序
- 使用共享內存時,每次操作都要進行存取和PV操作的開銷是很大的,所以要儘量減少存取操作和PV操作。例如使用局部變量存取和更新值,將最終結果存取到共享內存中等等——使用局部變量消除不必要的引用
- 當線程數多於內核數量的時候,會導致一個核上多個線程上下文切換導致的開銷,所以一般每個核上只運行一個線程
7、其他並行問題
- 線程不安全
- 解決線程不安全:修改原函數,加鎖+複製
-
- 可重入函數是線程安全的
- 顯式:傳值傳遞,數據引用都是本地自動棧
- 隱式:引用傳遞,調用線程時傳遞指向非共享數據的指針,調用本棧數據
- Linux系統提供大多數線程不安全函數的可重入版本,可重入版本的名字以_r結尾
- 可重入函數是線程安全的
- 競爭
- 線程之間有依賴性,當其一來的工作並不是想象中的樣子就會出錯
- 死鎖
- 信號量引入的運行時錯誤
- 一個線程被阻塞,等待一個永遠不爲真的條件,換句話說,程序死鎖是因爲每個線程都在等待其它線程執行一個根本不可能發生的V操作
- 避免:當使用二元信號量時,可以通過互斥鎖加鎖順序規則
- 給定所有互斥操作一個全序,每個線程以一種順序獲得互斥鎖並以相反的順序釋放