获取两个互斥锁并避免死锁

12
以下代码存在潜在死锁的风险,但似乎是必要的:为了安全地从一个容器复制数据到另一个容器,必须锁定两个容器以防止其他线程中发生的更改。

以下代码包含潜在死锁,但似乎是必要的:为了安全地将数据从一个容器复制到另一个容器,必须锁定两个容器以防止在其他线程中发生更改。

void foo::copy(const foo & rhs)
{
    pMutex->lock();
    rhs.pMutex->lock();
    // do copy
}

Foo有一个STL容器,"do copy"本质上是使用std::copy。如何在不引入死锁的情况下锁定两个互斥锁?


std::lock具有避免死锁的算法,将两个互斥量传递给它,比实现自己的算法更易读。 - Mellester
6个回答

17

对于foo的实例,需要对它们进行某种总体排序,并且始终按照递增或递减的顺序获取锁,例如 foo1->lock() 然后是 foo2->lock()

另一种方法是使用函数语义,而不是覆盖现有的实例,编写一个foo::clone方法来创建一个新的实例。

如果你的代码正在做许多锁定操作,那么可能需要使用复杂的避免死锁算法,例如 银行家算法


1
即使是像这个 vs rhs 的地址这么简单的东西也可以工作。始终先锁定地址较低的那一个。 - Kyle Butt
克隆只有在不复制的情况下才能正常工作,我认为隐式共享也行不通,但我会看一下。有趣的方法,Kyle。我看不出任何缺陷。 - pomeroy
建议他制作数据的临时副本是一个不错的解决方案。然而,std::lock已经提供了这样的死锁避免算法,并且更易读。 - Mellester

1

这个怎么样?

void foo::copy(const foo & rhs)
{
    scopedLock lock(rhs.pMutex); // release mutex in destructor
    foo tmp(rhs);
    swap(tmp); // no throw swap locked internally
}

这是异常安全的,而且相当线程安全。要想100%线程安全,您需要检查所有代码路径,然后再用另一组眼睛重新审查一遍,之后再次审查...


1

正如 @Mellester 所提到的,您可以使用 std::lock 来锁定多个互斥量以避免死锁。

#include <mutex>

void foo::copy(const foo& rhs)
{
    std::lock(pMutex, rhs.pMutex);

    std::lock_guard<std::mutex> l1(pMutex, std::adopt_lock);
    std::lock_guard<std::mutex> l2(rhs.pMutex, std::adopt_lock);

    // do copy
}

但请注意检查rhs是否不是*this,因为在这种情况下,std::lock会由于锁定相同的互斥量而导致UB。


你为什么不先使用 std::defer_lockstd::unique_lock<std::mutex>,然后再执行 std::lock(l1,l2); 呢?这是出于个人喜好还是风格原因吗? - Gizmo
我不认为std::lock是原子操作的。这样仍然可能导致死锁,对吗? - undefined
使用死锁避免算法,锁定给定的可锁定对象lock1,lock2,...,lockn,以避免死锁。 - undefined

-1
为避免死锁,最好等待两个资源都可以被锁定:
不知道您使用的是哪种互斥量 API,因此这里提供一些任意的伪代码。假设 can_lock() 仅检查是否可以锁定互斥量,并且 try_lock() 如果成功锁定则返回 true,如果互斥量已被其他人锁定则返回 false。
void foo::copy(const foo & rhs)
{
    for(;;)
    {
        if(! pMutex->cany_lock() || ! rhs.pMutex->cany_lock())
        {
            // Depending on your environment call or dont call sleep()
            continue;
        }
        if(! pMutex->try_lock())
            continue;
        if(! rhs.pMutex->try_lock())
        {
            pMutex->try_lock()
            continue;
        }
        break;
    }
    // do copy
}

4
为了避免死锁,最好引入一个活锁并使用自旋来占用100%的CPU。 - bk1e

-1

这是一个已知的问题,已经有了标准解决方案。 std::lock() 可以同时在2个或更多互斥量上调用,同时避免死锁。 在此处获取更多信息 它提供了推荐。

std::scoped_lock 为此函数提供了 RAII 包装器,并且通常优先于对 std::lock 的裸调用。

当然,这并不真正允许在另一个锁之上早期释放一个锁,因此像我在这个 类似问题的答案 中所做的那样,可以使用 std::defer_lockstd::adopt_lock


-2

您可以尝试使用scoped_lock或auto_lock同时锁定两个互斥量...就像银行转账一样...

void Transfer(Receiver recv, Sender send)
{
    scoped_lock rlock(recv.mutex);
    scoper_lock slock(send.mutex);

    //do transaction.
}

这是一场灾难的配方。 - Mellester

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接