std::lock 如何与 std::unique_lock 对象一起使用,而不是直接与 std::mutex 一起使用?

How does std::lock work with std::unique_lock objects instead of directly with std::mutex?

提问人:Sami 提问时间:9/10/2023 最后编辑:Remy LebeauSami 更新时间:9/10/2023 访问量:95

问:

我正在使用一段涉及银行账户转账的多线程代码。目标是在不遇到竞争条件的情况下安全地在账户之间转移资金。我用于在转账期间保护银行账户余额:std::mutex

我的问题围绕着 with 的使用 。我不是将对象直接传递给 ,而是将它们包装起来并将它们传递给 。std::unique_lockstd::lockstd::mutexstd::lockstd::unique_lockstd::lock

如何使用对象?std::lockstd::unique_lock

负责实际锁定和互斥锁,而对象仅管理锁(即,当它们超出范围时释放它们)?std::lockfromtostd::unique_lock

调用方法吗?std::locklock()std::unique_lock

与直接将对象传递给相比,使用有什么好处?std::unique_lockstd::lockstd::mutexstd::lock

struct bank_account
{
    bank_account(int balance) :
        mtx(), balance{ balance }

    {}
    std::mutex mtx;
    int balance;
};

void transfer(bank_account& from, bank_account& to, int amount)
{
    std::unique_lock<std::mutex> from_Lock(from.mtx, std::defer_lock);
    std::unique_lock<std::mutex> to_Lock(to.mtx, std::defer_lock);
    std::lock(from_Lock, to_Lock);
    
    if (amount <= from.balance)
    {
        std::cout << "Before:    " << amount << " from: " << from.balance << " to: " << to.balance << '\n';
        from.balance -= amount;
        to.balance += amount;
        std::cout << "After:     " << amount << " from: " << from.balance << " to: " << to.balance << '\n';
    }
    else
    {
        std::cout << amount << " is greater than " << from.balance << '\n';
    }
}

int main()
{
    bank_account A(200);
    bank_account B(100);
    std::vector<std::jthread> workers;
    workers.reserve(20);
    for (int i = 0; i < 10; ++i)
    {
        workers.emplace_back(transfer, std::ref(A), std::ref(B), 20);
        workers.emplace_back(transfer, std::ref(B), std::ref(A), 10);
    }
}
C++ 多线程死 unique-lock

评论

4赞 Remy Lebeau 9/10/2023
您是否阅读了 cpppreference 关于 std::unique_lock 和 std:lock() 的文档?
0赞 Sami 9/10/2023
我确实读过,但这就是我在下面的答案中想知道的,我在资源中找不到它:“当 std::unique_lock 的对象传递给 std::lock 时,std::lock 函数调用每个 std::unique_lock 对象的 lock() 方法。这反过来又会锁定关联的互斥锁。在这种情况下,由于 std::d efer_lock,互斥锁不会在创建 std::unique_lock 对象时直接锁定。相反,它通过 std::lock 函数锁定。

答:

5赞 Jan Schultke 9/10/2023 #1

std::lock 的目的是提供多个可锁定对象的无死锁锁定(参见 libc++ 实现)。 经典问题是,如果您有两个锁 L1L2,并且

  • 一个线程先锁定 L1,然后锁定 L2,然后
  • 另一个线程锁定 L2,然后锁定 L1

然后可能会出现死锁,因为每个线程可以保存一个锁,而另一个线程需要另一个锁。当您锁定并进入以下内容时,此问题适用:from.mtxto.mtx

std::unique_lock<std::mutex> from_Lock(from.mtx, std::defer_lock);
std::unique_lock<std::mutex> to_Lock(to.mtx, std::defer_lock);
std::lock(from_Lock, to_Lock);

std::lock执行 和 的无死锁锁定,并执行其余部分(即 RAII 内容)。from_Lockto_Lockstd::unique_lock

Q&A问答

如何使用对象?
调用方法吗?
std::lockstd::unique_lockstd::locklock()std::unique_lock

std::unique_lock可锁定的,并将调用它,然后是互斥锁。std::locklock()lock()

负责实际锁定和互斥锁,而对象仅管理锁(即,当它们超出范围时释放它们)?std::lockfromtostd::unique_lock

std::unique_lock完全能够自行锁定和解锁互斥锁。它唯一不能做的就是在涉及多个锁时实现无死锁锁定。

与直接将对象传递给相比,使用有什么好处?std::unique_lockstd::lockstd::mutexstd::lock

之后,您必须手动解锁两个互斥锁,这很容易出错。这是一个与 vs. / 类似的问题。如果您立即将两个互斥锁包装在一个 不过,那就好了。std::unique_ptrnewdeletestd::lock_guard

进一步改进

对于 ,您可以使用比以下更简单的锁:std::lockstd::unique_lock

std::lock(from.mtx, to.mtx);
std::lock_guard<std::mutex> from_lock(from.mtx, std::adopt_lock);
std::lock_guard<std::mutex> to_lock(to.mtx, std::adopt_lock);

您只需要在想转让所有权时;否则,您可以使用(这是一个稍微简单的类型)。std::unique_lockstd::lock_guard

如果您使用的是 C++17,则使用 std::scoped_lock 会变得更加简单:

// CTAD, equivalent to std::scoped_lock<std::mutex, std::mutex> lock(...)
std::scoped_lock lock(from.mtx, to.mtx);

std::scoped_lock是构造函数的替代品,并且具有内置于无死锁的锁定,类似于使用 。std::lock_guardstd::lock


Смотритетакже: 锁定多个 std::mutex 的最佳方法是什么?

评论

0赞 Sami 9/10/2023
非常感谢您提供详细且非常有用的解释。根据您的解释,我开始理解,当 std::unique_lock 的对象传递给 std::lock 时,std::lock 函数会调用每个 std::unique_lock 对象的 lock() 方法。这反过来又会锁定关联的互斥锁。在这种情况下,由于 std::d efer_lock,互斥锁不会在创建 std::unique_lock 对象时直接锁定。相反,它通过 std::lock 函数锁定,该函数旨在处理无死锁场景。我的理解准确吗?
1赞 Jan Schultke 9/10/2023
@Sami是的,这听起来是对的。请记住,这将调用 的混合物,直到它成功锁定所有锁。std::locklock()try_lock()