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

Deadlock simulation using std::mutex

我有以下示例:

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

在这种情况下,我遇到了僵局。但是当我使用 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);
}

你的两个代码片段没有可比性。第二个片段锁定并立即解锁每个互斥量,因为临时 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相同的算法,因此两者可以兼容使用。

虽然 Kerrek SB 的回答是完全正确的,但我想我会另辟蹊径。 std::lock 或任何尝试并撤退死锁避免策略应被视为从性能角度来看的最后手段。

怎么样:

#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!