2017-04-21 106 views
2

我有下面的例子:死鎖模擬::互斥

template <typename T> 
class container 
{ 
public: 
    std::mutex _lock; 
    std::set<T> _elements; 

    void add(T element) 
    { 
     _elements.insert(element); 
    } 

    void remove(T element) 
    { 
     _elements.erase(element); 
    } 
}; 

void exchange(container<int>& cont1, container<int>& cont2, int value) 
    { 
     cont1._lock.lock(); 
     std::this_thread::sleep_for(std::chrono::seconds(1)); 

     cont2._lock.lock(); 

     cont1.remove(value); 
     cont2.add(value); 

     cont1._lock.unlock(); 
     cont2._lock.unlock(); 
    } 

    int main() 
    { 
     container<int> cont1, cont2; 

     cont1.add(1); 
     cont2.add(2); 

     std::thread t1(exchange, std::ref(cont1), std::ref(cont2), 1); 
     std::thread t2(exchange, std::ref(cont2), std::ref(cont1), 2); 

     t1.join(); 
     t2.join(); 

     return 0; 
    } 

在這種情況下,我expiriencing死鎖。但是,當我使用std :: lock_guard而不是手動鎖定和解鎖互斥鎖時,我沒有任何死鎖。爲什麼?

void exchange(container<int>& cont1, container<int>& cont2, int value) 
{ 
    std::lock_guard<std::mutex>(cont1._lock); 
    std::this_thread::sleep_for(std::chrono::seconds(1)); 

    std::lock_guard<std::mutex>(cont2._lock); 

    cont1.remove(value); 
    cont2.add(value); 
} 
+0

這並不直接回答你的問題,但你可以通過使用['std :: lock'](http://en.cppreference.com/w/cpp/thread/lock)來鎖定這兩個死鎖互斥一次。你可以(也應該)在使用'std :: adopt_lock'之後,仍然可以將鎖的所有權轉移給'lock_guard'。 – ComicSansMS

+0

@Amadeusz。你爲什麼改變論據?這是你陷入僵局的原因(或者你故意模擬它)。 –

+0

@WernerErasmus:提示:看問題標題:-) –

回答

7

你的兩個代碼片段沒有可比性。第二個片段的鎖,並立即解鎖每個互斥體作爲臨時lock_guard對象的分號被破壞:

std::lock_guard<std::mutex>(cont1._lock); // temporary object 

使用鎖警衛正確的方法是,使作用域變量他們

{ 
    std::lock_guard<std::mutex> lock(my_mutex); 

    // critical section here 

} // end of critical section, "lock" is destroyed, calling mutex.unlock() 

(請注意,是另一個常見的錯誤是類似的,但不同的:

std::mutex mu; 
// ... 
std::lock_guard(mu); 

聲明一個名爲mu的變量(就像int(n);)。但是,此代碼格式不正確,因爲std::lock_guard沒有默認構造函數。但它會編譯,例如,std::unique_lock,它也不會鎖定任何東西。)

現在解決真正的問題:如何以一致的順序一次鎖定多個互斥鎖?就整個代碼庫甚至整個未來用戶的代碼庫甚至在本地情況下(如您的示例所示)達成一致鎖定順序可能不太可行。在這樣的情況下,使用std::lock算法:

std::mutex mu1; 
std::mutex mu2; 

void f() 
{ 
    std::lock(mu1, mu2); 

    // order below does not matter 
    std::lock_guard<std::mutex> lock1(mu1, std::adopt_lock);   
    std::lock_guard<std::mutex> lock2(mu2, std::adopt_lock); 
} 

在C++ 17有一個稱爲scoped_lock一個新的可變參數鎖定擋板模板:

void f_17() 
{ 
    std::scoped_lock lock(mu1, mu2); 

    // ... 
} 

scoped_lock構造函數使用相同的算法std::lock,所以兩者可以兼容使用。

+2

嗯,我錯過了(沒有閱讀,只是假設他正在使用lock_guard)。 –

+0

我能否提出一個從「不可行」到「如果不可行」的小改變?重要的問題是關於嵌套的順序而不是鎖定的順序,並且很可能實現多個協同操作的庫,這些庫在進入時鎖定事物並在退出時解鎖並且永不衝突。該策略的經典失敗是多態。只要多態方法明確啓動或(更糟糕!)在調用者執行相同操作時隱式獲取鎖,鵝就可能按照規定的鎖定順序進行烹飪。 – Persixty

+0

@DanAllen:嗯,我已經說過「整個代碼庫」;在特定情況下就訂單達成一致是肯定可行的。 OP的例子表明順序和嵌套一樣重要。也許最好提到嵌套問題是另一個問題的來源? –

1

雖然Kerrek SB的回答是完全有效的,但我認爲我會在環中拋出另一個帽子。或者任何嘗試和撤退的避免死鎖策略應該被看作是從性能角度來看最後的手段。

如何:

#include <functional> //includes std::less<T> template. 

static const std::less<void*> l;//comparison object. See note. 

void exchange(container<int>& cont1, container<int>& cont2, int value) 
    { 
     if(&cont1==&cont2) { 
      return; //aliasing protection. 
     } 
     std::unique_lock<std::mutex> lock1(cont1._lock, std::defer_lock); 
     std::unique_lock<std::mutex> lock2(cont2._lock, std::defer_lock); 
     if(l(&cont1,&cont2)){//in effect portal &cont1<&cont2 
      lock1.lock(); 
      std::this_thread::sleep_for(std::chrono::seconds(1)); 
      lock2.lock(); 
     }else{ 
      lock2.lock(); 
      std::this_thread::sleep_for(std::chrono::seconds(1)); 
      lock1.lock(); 
     } 
     cont1.remove(value); 
     cont2.add(value); 
    } 

該代碼使用對象的內存地址來確定一個任意但一致的鎖順序。這種方法可以(當然)被推廣。

還要注意,在可重用代碼中,別名保護是必需的,因爲cont1爲cont2的版本將通過嘗試鎖定相同的鎖兩次而無效。不能假定爲std::mutex是遞歸鎖,通常不是。

注意:使用std::less<void>可以確保遵從性,因爲它可以保證地址的總體排序一致。技術上(& cont1 < & cont2)是未指定的行爲。感謝Kerrek SB!

+1

'&cont1 <&cont2'具有未定義的行爲,不幸的是:-S –

+0

您的演員是否會編譯? –

+0

@KerrekSB添加了一些關於可移植性的註釋。我認爲,排除一個非常有效的策略是可恥的,這種策略幾乎可以在所有平臺上運行,以獲得關於指針位模式的技術正確但難以理解的關注。我只能想到在一些奇怪的分頁內存平臺上這個失敗。你知道任何物理平臺,它不會工作嗎? – Persixty