【问题标题】:Why would I want to lock two mutexes in one function - that too with deferred lock?为什么我想在一个函数中锁定两个互斥锁——延迟锁定也是如此?
【发布时间】:2018-08-17 07:16:03
【问题描述】:

https://en.cppreference.com/w/cpp/thread/lock_tag

void transfer(bank_account &from, bank_account &to, int amount)
{
    // lock both mutexes without deadlock
    std::lock(from.m, to.m);
    // make sure both already-locked mutexes are unlocked at the end of scope
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);

// equivalent approach:
//    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
//    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//    std::lock(lock1, lock2);

    from.balance -= amount;
    to.balance += amount;
}

一次锁定两个互斥锁有什么好处?
他们在这里通过延迟锁获得了什么?

请解释他们做出这个决定的原因。

【问题讨论】:

    标签: c++ std mutex


    【解决方案1】:

    如果我修改银行帐户而不锁定它,其他人可能会同时尝试修改它。这是一场竞赛,结果将是未定义的行为(通常是丢失或魔法创造的金钱)。

    转账时,我正在修改 2 个银行账户。所以他们都需要被锁定。

    问题是,当锁定多个事物时,每个 locker 必须以相同的顺序锁定和解锁,否则就会出现死锁。

    当它是银行账户时,没有自然的锁顺序。数以千计的线程可以向各个方向转移资金。

    因此,我们需要一种锁定多个互斥体的方法来解决这个问题 - 这是std::lock

    std::lock 只是锁定互斥体 - 它不保证在退出当前代码块时解锁。

    std::lock_guard&lt;&gt; 在销毁时解锁它所指的互斥锁(参见 RAII)。这使得代码在所有情况下都能正确运行 - 即使存在可能导致从当前代码块提前退出而没有代码流过语句(例如 to.m.unlock()

    )的异常

    这里有一个很好的解释(带有示例):https://wiki.sei.cmu.edu/confluence/display/cplusplus/CON53-CPP.+Avoid+deadlock+by+locking+in+a+predefined+order

    【讨论】:

    • 您总是可以先锁定具有较低(或较高)帐号的帐户,这已经可以防止死锁。
    • 感谢多锁的解释。如果您可以包括使用延迟锁来完成答案,那就太好了。
    【解决方案2】:

    Richard Hodgesanswer 上的扩展名

    一次锁定两个互斥锁有什么好处?

    Richard 已经很好地解释了,只是稍微明确一点:我们通过这种方式避免死锁(std::lock 的实现是为了不会发生死锁)。

    他们在这里通过延迟锁获得了什么?

    延迟锁定会导致无法立即获得它。这很重要,因为如果他们这样做,他们只会在没有任何防止死锁(随后的std::lock 实现)的情况下这样做。

    关于避免死锁(见std::lock):

    使用死锁避免算法锁定给定的可锁定对象 lock1, lock2, ..., lockn 以避免死锁。

    对象被一系列未指定的 lock、try_lock 和 unlock 调用锁定。 [...]

    旁注:另一个更简单的避免死锁的算法总是用 e 锁定银行帐户。 G。首先降低帐号(AN)。如果一个线程正在等待更高 AN 的锁,那么持有它的另一个线程要么已经获得了两个锁,要么正在等待第二个 - 这不能是第一个线程的一个,因为它必须具有更高的 AN .

    这对于任意数量的线程并没有太大变化,任何持有较低锁的线程都在等待较高的锁,如果也持有的话。如果你绘制一个从 A 到 B 的有向图,如果 A 正在等待 B 持有的第二个锁,你会得到一个(多)树结构,但你永远不会有圆形子结构(这表明死了锁)。

    【讨论】:

    • 谢谢。这意味着 std::lock 是唯一实现了死锁避免算法的锁。 std::unique_lock 没有这个避免死锁的特性?
    • @Aquarius_Girl 不,它没有。 std::unique_lock 旨在仅锁定一个互斥锁 - 恰好在您创建它(未延迟)或通过调用 lock 或最终 try_lock 告诉它(延迟)时。同样适用于std::lock_guard(除了不提供延迟锁)。如果将构造函数(接受一个互斥量参数)与函数的参数(多个可锁定对象)进行比较,这种差异会变得很明显。
    • @Aquarius_Girl 让我明确地向您提示一方面类之间更基本的区别(根据 RAII 原则解锁析构函数)和 std::lock 是“仅”功能,没有解锁功能...
    • 这里实际做的是结合三个工具用于不同的目的:1. mutex 用于线程同步/基本(实际)锁定 2. lock_guard/unique_lock 用于安全解锁 3. std::lock 用于死锁安全(避免)
    • 关于解锁:根本没有必要。你会如何在释放锁时陷入死锁?那是不可能的......所以你可以在你不再需要它们时以任意顺序释放你的锁 - 你可以通过使用守卫来保证。
    【解决方案3】:

    银行账户数据结构对每个账户都有一个锁。

    将资金从一个帐户转移到另一个帐户时,我们需要锁定两个帐户(因为我们要从一个帐户中取出资金并将其添加到另一个帐户)。我们希望这个操作不会死锁,所以使用std::lock 同时锁定这两个操作,因为这样做可以确保没有死锁。

    完成事务后,我们需要确保释放锁。此代码使用 RAII 执行此操作。使用adopt_lock 标记,我们使对象采用已锁定的互斥体(当lock1 超出范围时将释放该互斥体)。 使用defer_lock 标记,我们为当前解锁的互斥体创建unique_lock,目的是稍后将其锁定。同样,当unique_lock 超出范围时,它将被解锁。

    【讨论】:

      【解决方案4】:

      fromto2 帐户,可以在应用程序的任何地方单独使用。

      通过为每个帐户设置互斥锁,您可以确保在您进行转移时没有人使用fromto 帐户。

      lock_guard 将在退出函数时释放互斥锁。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多