c++多線程-線程中的死鎖問題


假設有一個玩具,有兩部分組成。一部分是鼓另一部分是鼓錘,任何人他們想玩這個玩具的話必須要擁有這個玩具的兩部分(鼓和鼓錘)。

現在假設你有兩個孩子都喜歡玩這個玩具,如果其中一個孩子同時拿到鼓和鼓錘他可以快樂的玩耍,直到他玩累了不玩了。如果另一個孩子想要玩這個玩具必須等前一個孩子玩完才可以玩,盡管他不高興。由於鼓和鼓錘是分開裝在兩個玩具盒,此時你的兩個孩子同時想要玩這個玩具,兩個孩子翻找玩具拿,一個人找到了鼓另一個找到了鼓錘,現在他們兩個卡住都玩不了,除非一個很高興的把玩具的一部分讓給另外一個,讓另一個玩。但是孩子都比較調皮,不懂得了謙讓。每個人都堅持自己要玩,最后的問題是他們一直疆持,誰也玩不了。

想象一個,你沒有孩子爭玩具,而是兩個線程爭互斥鎖:每一個線程需要去鎖定一對互斥鎖(mutex)去執行一個任務,一個線程已經獲取了一個互斥鎖(mutex),另一個也獲取了一個互斥鎖(mutex),每一個線程都在等待鎖定另一個互斥鎖(mutex),這樣造成的結果是 任何一個線程都無法得到另一個互斥鎖(mutex)。這種就叫死鎖(deadlock),這里最大的問題是完成任務必須鎖定兩個或多個互斥鎖(mutex)來執行操作。

這種情況通常的建議是按相同的順序來鎖定多個互斥鎖(mutex),如果你一直先鎖定mutex A再鎖定mutex B,那一定不會發生死鎖。有時這很簡單,因為互斥對象有不同的用途,但有時卻不那么簡單,例如互斥對象分別保護同一個類的單獨對象時。例如有個操作是交換同一個類的兩個對象。為了保證他們兩個個都交換成功,避免被多線程修改。兩個對象的mutex都要鎖定。如果一固定的順序去鎖定(例如先鎖定第一個參數對象,再鎖定第二個參數對象)這會適得其反。兩個線程同時嘗試交換同兩個對象時,就會發生死鎖。

幸好C++標准庫有一個解決方法就是用 std::lock 一次去鎖定多個mutex,這樣不會讓程序死鎖。如圖示例如示:

  1 class some_big_object;
  2 void swap(some_big_object& lhs,some_big_object& rhs);
  3 class X
  4 {
  5 private:
  6  some_big_object some_detail;
  7  std::mutex m;
  8 public:
  9  X(some_big_object const& sd):some_detail(sd){}
 10  friend void swap(X& lhs, X& rhs)
 11  {
 12  if(&lhs==&rhs)
 13  return;
 14  std::lock(lhs.m,rhs.m);
 15  std::lock_guard lock_a(lhs.m,std::adopt_lock);
 16  std::lock_guard lock_b(rhs.m,std::adopt_lock);
 17  swap(lhs.some_detail,rhs.some_detail);
 18  }
 19 };

上面代碼中 std::adopt_lock 代表該mutex已經lock了。這樣確保這些mutex被正確的鎖定(受保護的操作可能會引發異常)在函數退出時正常解鎖。另外值得注意的是 在對std::lock的調用中鎖定兩個mutex時有可能會拋出異常,在這種情況下異常將會被std::lock傳遞下去。如果std::lock已經成功鎖定了一個,在鎖定第二個的時候拋出異常了。那么第一個lock也會正常解鎖。std::lock確保要全部鎖定和解除鎖定。

雖然std::lock可以幫助你解決多個mutex同時需要鎖定的情形,但他沒法去幫助你分開鎖定兩個mutex。這種情況你要根據開發人員制定的規則和代碼流程來確定不會發生死鎖。但是這不那么簡單:線程死鎖是一個比較棘手的問題,而且在多線程代碼中經常會碰到。有時候在開發測試的時候沒有問題,但是在運行時會出現死鎖。但是,有一些相對簡單的規則可以幫助你編寫無死鎖的代碼。

避免死鎖的一般准則:

線程死鎖只發生在有鎖的情況下。有時你創建了兩個個線程,這兩個線程分別join另一個線程,這樣任何join都無法返回,每個線程都在等待另一個線程結束,這就是兩個孩子為了玩具打架一樣。這種的問題可以發生在任何“一個線程正在等另一個線程做事,而另一個線程有時候也會等第一個線程做什么事”的時候。避免線程死鎖歸結為一個重要概念就是:A線程不要等待B線程,如果B線程有可能等待A線程。

1.避免嵌套鎖定
這一條是最簡單的,你已經鎖定了一個mutex的時候,你最好不要再次鎖定。如果你遵守了這條規則,因為一個線程只有一個鎖的情況下不會造成死鎖。但是也有其它原因會造成死鎖(比如一個線程在等待另一個線程),如果你要鎖定多個,你就用std::lock。

2.在已經持有鎖的時候不要調用用戶自義的代碼
因為用戶自定義的代碼是無法預知的,誰知道他的代碼里會不會也想要鎖定這個lock。有時候無法避免不調用用戶定義代碼,這種情況下,你需要注意。

3.按固定順序鎖定
如果你要鎖定兩個以上的mutex而你又不能用std::lock。那么最好的建議就按固定順序去鎖定。

4.用層鎖來防止死鎖
hierarchical_mutex規則思想是:將mutex分層,規定加鎖順序是由高層到底層才能進行,底層到高層報出運行時錯誤,這樣就可以利用編程的方法檢測死鎖。書中實現了hierarchical_mutex類作為可分層的mutex,先列出使用方法如下

  1 hierarchical_mutex high_level_mutex(10000);
  2 hierarchical_mutex low_level_mutex(500);
  3 void ThreadA(){
  4 	std::lock_guard<hierarchical_mutex> lock1(high_level_mutex);
  5 	...    //做一些使用high_level_mutex就可以干的事     
  6 	std::lock_guard<hierarchical_mutex> lock1(low_level_mutex);
  7 	...    //需要兩個mutex同時加鎖才可以干的事
  8 
  9 }
 10 
 11 void ThreadB(){
 12 	std::lock_guard<hierarchical_mutex> lock1(low_level_mutex);
 13 	...    //做一些使用low_level_mutex就可以干的事         
 14 	//對高低層mutex加鎖的情況下,對高層mutex加鎖,不符合規定的順序,拋出異常!         
 15 	std::lock_guard<hierarchical_mutex> lock1(high_level_mutex);
 16 }


5.這些准則超越了束縛
正如我在本節開頭提到的那樣,死鎖不僅發生在鎖任何可能導致等待周期的同步結構都可能發生這種情況。它的因此,值得將這些准則擴展到涵蓋這些情況。例如,只是因為您應該盡可能避免獲取嵌套鎖,所以等待一個線程同時持有鎖,因為該線程可能需要獲取鎖為了繼續。同樣,如果您要等待線程完成,則可能是值得標識線程層次結構,以便線程僅等待較低的線程在層次結構中。一種簡單的方法是確保您的線程設計完避免死鎖的代碼后,請執行std :: lock()和std ::lock_guard涵蓋了大多數簡單鎖定的情況,但有時更具靈活性是必須的。對於這些情況,標准庫提供了std :: unique_lock模板。像std :: lock_guard一樣,這是在互斥鎖上參數化的類模板類型,並且還提供與std :: lock_guard相同的RAII樣式的鎖管理但靈活性更高。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM