併發思想提煉(1)(理解併發,避免死鎖) 一直做伺服器後端和基礎組件平臺開發,常常用到併發,故簡單放些乾貨,一來算是總結,二來希望後人少走彎路, 寫到哪兒算哪兒,不定期更新。 1. Introduction 先來明白一些概念。Concurrency併發和Multi-thread多線程不同 你在
併發思想提煉(1)(理解併發,避免死鎖)
一直做伺服器後端和基礎組件平臺開發,常常用到併發,故簡單放些乾貨,一來算是總結,二來希望後人少走彎路, 寫到哪兒算哪兒,不定期更新。
1. Introduction
先來明白一些概念。Concurrency併發和Multi-thread多線程不同
你在吃飯的時候,突然來了電話。
- 你吃完飯再打電話,這既不並行也不多線程
- 你吃一口飯,再打電話說一句話,然後再吃飯,再說一句話,這是併發,但不多線程。
- 你有2個嘴巴。一個嘴巴吃飯,一個嘴巴打電話。這就是多線程,也是併發。
併發:表示多個任務同時執行。但是有可能在內核是串列執行的。任務被分成了多個時間片,不斷切換上下文執行。
多線程:表示確實有多個處理內核,可同時處理多個任務。
世界的複雜的,世界是併發的。於是模擬這世上的業務的程式也是併發的。隨著系統功能的增加,複雜度不斷提高,併發特性被引入編程。
2. 最簡單的並行
兩個任務互不幹擾,它們不會影響到系統的同一實體。每個線程只需要自己做好自己的任務即可。
兩個任務會影響到同一實體,但是不會在同時訪問該同一實體。這樣,在這個實體上,任務是串列執行的。這樣也是安全的併發。
3. 危險的並行
兩個任務同時訪問同一實體,臟讀寫的問題,設有a值為1, 兩個線程先後加1,按道理說最後a值結果為3.
- T1線程讀取數據a;T2線程讀取數據a;
- T1線程a++, 然後反存到a,a值為2;
- T2線程a++, 然後反存到a, a值還是為2;
兩次操作後,a值不為3,而是2。這就是併發出現的錯誤。a若是一個可釋放(disposable)的實體. T1線程釋放a,T2線程操作a,會造成更大的錯誤。
4. 如何避免此危險
1. 乾脆就串列執行T1,T2。不過沒有利用到處理器的併發特性。雖然安全,但是效率不高。
2. T1,T2併發。但是不會同時操作同一個實體(Critical Entity)。即實體不是併發的。實體是串列的。
3. 讀取關鍵實體使用Clone,拷貝出來的實體是臨時的。本次操作在該臨時實體上。下次操作繼續Clone該實體再使用。
經常能用到的是方法2和方法3。接下來具體說說方法2和3。
5. 讓對關鍵實體的訪問串列
方法2的核心思想是串列。不過不是任務串列,而是訪問共用實體時串列。串列是人類容易思維的方式,把併發問題轉變為順序執行問題,也助於後來維護人員理解。一個最簡單的實現方式就是加Lock then access。
6. Lock地獄
Lock是併發程式中常用的操作,每個人都會用。。。。嗯。。。。常常會濫用。。。然後,運行一段時間。嘣,程式自爆。說說常常遇到的問題:
- 死鎖。T1 lock A request B,T2 lock B request A。T1和T2被互相Block住。程式進行不了
- 遞歸鎖。T1 lock A and lock A again。同一線程T1被自己Block住。
遞歸鎖好解決,C++ 11中有std::recursive_mutex。再高級一點的語言自帶這樣的特性。比如C#中的lock就自帶遞歸鎖。一般地,遞歸鎖通過裡面添加counter實現,每次鎖就counter++,每次release就counter--。Counter 為0就表示解鎖。其實這就是一種信號量(semaphore)的實現方法。引申開來去,C++中的share_ptr/auto_ptr就是此類思想,這種通過引用計數來判斷該對象是否被使用,若檢測到不被使用從而自動的release該段記憶體。C#和Java中記憶體管理就是這個思路。不多講了,以後單開段子講。
7. 死鎖及其防止
確切的說死鎖單純在程式這個層面難以防止,最好在設計開始就註意這個問題。就是說在程式設計的時候就搞明白那些資源會互相調用。這樣的情況就要多留心眼。我工作中一直寫一些基礎架構API。當其它工程師調用時。。。。。嗯。。。。。不仔細看文檔,在一些不適用的地方使用造成了死鎖,然後來找我。。。。。這種情況下要麼就多培訓,多強調使用手冊。要麼,這樣說吧,把API寫得健壯點。畢竟完全不會知道用戶會怎麼使用。這就是我在API開發中使用靜態語言的原因(其實我更喜歡動態語言python,用python做leetcode真是心情舒暢),至少在編譯階段就能有一定程度的規則控制,而不是到了運行階段報Error。這關係到如何編寫健壯的API,以後開段子講。
回到死鎖防止這個問題,關於避免死鎖的API可以使用這個方法---try lock機制。簡單就是說給lock一個時間鎖,如果在一段時間內還是沒有取得該資源的訪問權就跳過,放LOG,然後執行下麵的步驟。可以通過前面講的“等待信號量”來實現此機制,其實也不用自己特別實現,各主流語言應該都有。核心思想在軟體層面上是放個自旋鎖和Flag量,覺得搞不明白(C++,C#,Java等該功能實現方式)的話自己也可以實現一個。
(to be continue....)