多線程出現
傳統的進程都是單線程的程序。我們總是希望自己的程序有更高的並行性,傳統進程也是有辦法實現這種並行性,那就是通過子進程,但是子進程是獨立的數據空間,很多時候程序的不同任務都是需要訪問相同數據的,因此子進程有很大的侷限性。
在這種需求場景下,多線程出現了,它彌補了子進程的缺陷,因爲進程內的線程共享進程的資源,可以很容易實現數據共享。
線程概念
線程在進程內部,它共享進程的地址空間。
線程屬性
線程共享執行代碼,data分段和打開的文件。但是有獨立寄存器、程序計數器、狀態和棧。第一列是線程共享的進程屬性,第二列是線程獨立的屬性。
線程和進程關係
下圖是一個進程包含3個線程
線程狀態
同傳統進程一致
多線程實現
用戶態多線程
- 用戶態多線程結構
- Run-Time System:用戶態實現的多線程,線程的創建、調度、銷燬都是在用戶態,因此運行時系統負責管理線程調度,每個進程都有一個運行時系統
- Thread table:線程表,記錄當前進程擁有的線程
- 用戶態多線程的優點
- 線程切換不需要切換上下文
- 線程切換不需要刷新緩存
- 可以在不支持線程的系統中實現多線程
- 用戶態多線程的缺點
- 雖然實現了多線程,但是因爲對系統是透明的,系統並不知道進程是多線程,因此用戶態多線程並不能真正並行,只能利用單核
- 阻塞系統調用,當進程的一個線程調用了阻塞系統調用時,整個進程都會陷入阻塞狀態。內核線程阻塞了之後,無法通知進程運行時系統線程調度器導致的進程阻塞。解決方案是jacket,將阻塞系統調用替換爲非阻塞系統調用,比如同步的I/O替換爲異步I/O
- 缺頁中斷也會導致進程阻塞
- 用戶態線程因爲沒有時鐘中斷,因此不能實現輪轉調度
內核態多線程
- 內核態多線程結構
- 內核線程由系統調度
- 線程表,在內核空間
- 內核態多線程優點
- 可以利用多核cpu,能真正提升進程的並行
- 阻塞系統調用問題不會導致進程阻塞,因爲內核多線程是由系統調用,系統管理着線程,因此知道線程已阻塞,就可以調度其他線程執行
- 缺頁中斷時可以切換其他線程執行
- 內核態多線程缺點
- 內核線程每次切換都會陷入內核,從用戶態切換到內核態,這個過程是有開銷的
- 創建和銷燬線程開銷,線程複用
- 信號是發送給進程而不是線程
- 當線程創建新的進程時,新進程屬性(含有線程數)問題
混合實現
- 混合實現結構
混合模式下,一個內核進程可以映射多個用戶態線程
- 混合實現的優點
實現了最大的靈活性
線程調度激活機制
- 線程調度激活機制結構
- 避免阻塞
爲了避免用戶態線程在調用阻塞系統調用時阻塞進程,內核在發現線程執行阻塞調用時會通過upcall通知進程,進程run-time system設置當前線程爲block狀態,然後調度其他線程運行;當阻塞操作調用完成會再次通知進程,run-time system可以選擇立即運行或者加入就緒隊列 - 硬件中斷
如果進程對該中斷不感興趣,可能是其他進程的I/O完成,run-time system可以忽略,然後恢復線程到中斷前的狀態繼續運行;如果進程對該中斷感興趣,比如可能是某個線程的缺頁加載完成,此時運行哪個線程取決於run-time system
多線程程序設計的挑戰
識別任務
識別出可以獨立、併發的任務,可以獨立運行在多核處理器上
平衡
考慮多核運行是否值得, 根據Amdahl定律,一個應用通過增加cpu可以獲得的加速,S代表任務必須串行執行佔比,N代表核心數
一個例子,橫座標是核心數,縱座標是加速倍數,曲線代表不同必須串行執行佔比在不同核心數下的加速
數據劃分
如果應用劃分爲獨立的任務,那麼任務訪問和操作的數據必須劃分到不同的cpu上
數據依賴
如果並行的任務訪問的數據之間有依賴,需要同步機制來保證數據一致性,在後續的筆記中會詳細介紹線程同步
測試和調試
當應用是由多線程實現時,測試和調試都比較困難
多線程應用舉例
Java的多線程模型
瞭解完多線程後,很好奇Java的多線程模型是怎麼實現的呢?JVM中並沒有說明Java的多線程是用戶態實現還是內核態實現。網上查了一番資料後,感覺知乎的R大的回答比較靠譜一點,鏈接。在較新的Hotspot VM,在除了Solaris的平臺上實現的是1:1的模型,也就是內核態實現多線程。感興趣的可以點擊上面的鏈接查看詳情。
Go語言的goroutine
Go語言中的goroutine就是一種用戶態多線程,它有自己實現的調度器。
總結
多線程在提升性能的同時也給編程帶來了複雜性。不過在開發過程中,不管是用什麼語言,都會經過一些封裝,使其可用性更高。比如Java的JUC下的線程池,它已經封裝了很多細節,讓我們只需要關注任務和數據的拆分,而不用關注多線程實現的細節。