Go 協程與併發機制(二)併發程序
【思考】
● 併發程序難在哪裏?
我們在上一篇文章 Go 協程與併發機制(一)進程與線程中,從進程和線程的關係,講到多線程與多核 CPU 的關係,再回歸到併發和並行的定義。
到這裏我們可以想想寫一個併發程序究竟會遇到什麼困難,線程這樣的工具會完美解決這樣的問題麼?
線程的出現很好解決了早期互聯網發展後帶來的多用戶問題,如果一個用戶要起一個進程,進程間需要共享資源還得通過進程間的通信機制,會給系統帶來很大的性能消耗。
線程的使用比較簡單,如果你覺得這塊代碼需要併發,就把它放在單獨的線程裏執行,由系統負責調度,具體什麼時候使用線程,要用多少個線程,由調用方(操作系統)決定。
但定義方並不清楚調用方會如何使用自己的代碼,很多併發問題都是因爲誤用導致的,比如 Java 中的 Hashmap 就不是併發安全的,誤用會在多線程環境中導致問題。
01
併發環境使用線程
在本質上我們總結出,在併發環境中使用線程需要定義方規範兩個問題:
- 競態條件
不同線程間如何訪問共享數據進行資源讀寫。
- 依賴關係以及執行順序
如果線程之間的任務有依賴關係,需要等待以及通知機制來進行協調。
我們引用了很多複雜機制來保證以上兩個問題不再發生,比如 mutex 鎖、信號量、synchronized、volatile、CAS 等,同時建立嚴謹的 code review,全面的併發測試機制(golang 可以使用 -race 參數設置)。如果大家感興趣的話,可以瞭解在 Java 中如何通過線程去解決併發問題,推薦這篇文章:10 問 10 答:你真的瞭解線程池嗎?但是隨着併發度的不斷增加,更令人頭疼的是:
系統裏到底能承載多少線程?
02
併發環境中線程的弊端
一個比較通用的解答是從內存方面入手:
每個線程都需要一個棧(Stack)空間來保存掛起(suspending)時的狀態。線程的棧大小一般是創建時指定的,但由於線程是本質上也是進程,系統假定是要長期運行的,棧空間太小會導致稍複雜的遞歸調用(比如複雜點的正則表達式匹配)導致棧溢出,所以線程棧默認大小都會比較大,比如 Java 的棧空間(64 位 VM)默認是 1024k,不算別的內存,只是棧空間,啓動 1024 個線程就要 1G 內存。雖然可以用 -Xss 參數控制,但調整參數治標不治本。
另外有一個限制是 context-switch,即線程的調度成本,線程的調度大部分時間是搶佔式的,操作系統調度器爲了均衡每個線程的執行週期,會定時發出中斷信號強制執行線程的上下文切換。
換句話說就是線程在大規模創建並運行的場景裏會佔用更多的 CPU 和內存。
我們從以上的討論可以得出以下結論:
-
線程的成本較高(內存,調度)不可能大規模創建
-
應該由語言或者框架動態解決這個問題
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Ix_UaVENBy7E_gGyte0zOw