简体   繁体   English

使用std :: mutex进行死锁模拟

[英]Deadlock simulation using std::mutex

I have following example: 我有以下示例:

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;
    }

In this case I'm expiriencing a deadlock. 在这种情况下,我正在陷入僵局。 But when I use std::lock_guard instead of manually locking and unlocking mutextes I have no deadlock. 但是当我使用std :: lock_guard而不是手动锁定和解锁互斥锁时,我没有死锁。 Why? 为什么?

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);
}

Your two code snippets are not comparable. 您的两个代码段无法比较。 The second snippet locks and immediately unlocks each mutex as the temporary lock_guard object is destroyed at the semicolon: 第二个代码段锁定并立即解锁每个互斥锁,因为临时lock_guard对象在分号处被销毁:

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

The correct way to use lock guards is to make scoped variables of them: 使用锁定保护的正确方法是制作它们的范围变量

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

    // critical section here

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

(Note that there is another common error that's similar but different: (请注意,还有另一个常见错误,它们相似但不同:

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

This declares a variable named mu (just like int(n); ). 声明了一个名为mu 的变量 (就像int(n); )。 However, this code is ill-formed because std::lock_guard does not have a default constructor. 但是,此代码std::lock_guard ,因为std::lock_guard没有默认构造函数。 But it would compile with, say, std::unique_lock , and it also would not end up locking anything.) 但它会用std::unique_lock ,它也不会最终锁定任何内容。)

Now to address the real problem: How do you lock multiple mutexes at once in consistent order? 现在解决实际问题:如何以一致的顺序一次锁定多个互斥锁? It may not be feasible to agree on a single lock order across an entire codebase, or even across a future user's codebase, or even in local cases as your example shows. 在整个代码库中,甚至在未来用户的代码库中,或者甚至在本地案例中,就单个锁定订单达成一致可能是不可行的。 In such cases, use the std::lock algorithm: 在这种情况下,请使用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);
}

In C++17 there is a new variadic lock guard template called scoped_lock : 在C ++ 17中,有一个名为scoped_lock的新的可变参数锁定保护模板:

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

    // ...
}

The constructor of scoped_lock uses the same algorithm as std::lock , so the two can be used compatibly. scoped_lock的构造scoped_lock使用与std::lock相同的算法,因此两者可以兼容使用。

While Kerrek SB's answer is entirely valid I thought I'd throw an alternative hat in the ring. 虽然Kerrek SB的答案是完全有效的,但我以为我会在戒指中扔另一顶帽子。 std::lock or any try-and-retreat deadlock avoidance strategies should be seen as the last resort from a performance perspective. 从性能的角度来看,应该将std::lock或任何尝试和撤退死锁避免策略视为最后的手段。

How about: 怎么样:

#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);
    }

This code uses the memory address of the objects to determine an arbitrary but consistent lock order. 此代码使用对象的内存地址来确定任意但一致的锁定顺序。 This approach can (of course) be generalized. 这种方法(当然)可以概括。

Note also that in reusable code the aliasing protection is necessary because the version where cont1 is cont2 would be invalid by trying to lock the same lock twice. 另请注意,在可重用代码中,别名保护是必要的,因为cont1为cont2的版本会因尝试锁定两次相同的锁而无效。 std::mutex cannot be assumed to be a recursive lock and normally isn't. std::mutex不能被认为是递归锁定,通常不是。

NB: The use of std::less<void> ensures compliance as it guarantees a consistent total ordering of addresses. 注意:使用std::less<void>可确保合规性,因为它可确保地址的总排序一致。 Technically (&cont1<&cont2) is unspecified behavior. 技术上(&cont1 <&cont2)是未指明的行为。 Thanks Kerrek SB! 谢谢Kerrek SB!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM