死锁模拟::互斥

问题描述:

我有下面的例子:死锁模拟::互斥

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:提示:看问题标题:-) –

你的两个代码片段没有可比性。第二个片段的锁,并立即解锁每个互斥体作为临时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的例子表明顺序和嵌套一样重要。也许最好提到嵌套问题是另一个问题的来源? –

虽然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

+0

您的演员是否会编译? –

+0

@KerrekSB添加了一些关于可移植性的注释。我认为,排除一个非常有效的策略是可耻的,这种策略几乎可以在所有平台上运行,以获得关于指针位模式的技术正确但难以理解的关注。我只能想到在一些奇怪的分页内存平台上这个失败。你知道任何物理平台,它不会工作吗? – Persixty