Go 協程與併發機制(二)併發程序

【思考】

       ● 併發程序難在哪裏?

我們在上一篇文章 Go 協程與併發機制(一)進程與線程中,從進程和線程的關係,講到多線程與多核 CPU 的關係,再回歸到併發和並行的定義。

到這裏我們可以想想寫一個併發程序究竟會遇到什麼困難,線程這樣的工具會完美解決這樣的問題麼?

線程的出現很好解決了早期互聯網發展後帶來的多用戶問題,如果一個用戶要起一個進程,進程間需要共享資源還得通過進程間的通信機制,會給系統帶來很大的性能消耗。

線程的使用比較簡單,如果你覺得這塊代碼需要併發,就把它放在單獨的線程裏執行,由系統負責調度,具體什麼時候使用線程,要用多少個線程,由調用方(操作系統)決定。

但定義方並不清楚調用方會如何使用自己的代碼,很多併發問題都是因爲誤用導致的,比如 Java 中的 Hashmap 就不是併發安全的,誤用會在多線程環境中導致問題。

01

併發環境使用線程

在本質上我們總結出,在併發環境中使用線程需要定義方規範兩個問題:

  1. 競態條件

 不同線程間如何訪問共享數據進行資源讀寫。

  1. 依賴關係以及執行順序

 如果線程之間的任務有依賴關係,需要等待以及通知機制來進行協調。

我們引用了很多複雜機制來保證以上兩個問題不再發生,比如 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