unique_lock 当单个线程获取同一个互斥体的 2 unique_lock 时意味着什么?

what does unique_lock mean when a single thread acquire 2 unique_lock of the same mutex?

我有以下代码,来自https://en.cppreference.com/w/cpp/thread/unique_lock。然而,在打印输出时,我看到了一些意想不到的结果,并希望得到一些解释。

密码是:

#include <mutex>
#include <thread>
#include <chrono>
#include <iostream>
 
struct Box {
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int anotherNumber)
{
    // don't actually take the locks yet
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
 
    from.num_things += anotherNumber;
    to.num_things += anotherNumber;
    
    std::cout<<std::this_thread::get_id()<<" "<<from.num_things<<"\n";
    std::cout<<std::this_thread::get_id()<<" "<<to.num_things<<"\n";
    // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
 
int main()
{
    Box acc1(100);   //initialized acc1.num_things = 100
    Box acc2(50);    //initialized acc2.num_things = 50
 
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
 
    t1.join();
    t2.join();
}

我的期望:

  1. acc1 将初始化为 num_things=100,acc2 将初始化为 num_things=50.
  2. 假设线程 t1 先运行,它获得互斥锁 m,有 2 个锁。一旦锁被锁定,并且可以将 num_things 分配给 num=10
  3. 完成后,会依次打印from.num_things = 110和to.numthings = 60。先“从”,再“到”。
  4. thread1 完成代码的关键部分,wrapper unique_lock 调用它的析构函数,基本上解锁互斥量。

这是什么我不明白

我希望先解锁 lock1 填充,然后再解锁 lock2。 线程 t2 然后以相同的顺序获取互斥量并首先锁定 lock1,然后锁定 lock2。它还将按顺序运行关键代码,直到 cout。

线程 t2 将从 t1 获取全局 acc1.num_things = 110 和 acc2.num_things = 60。

我预计 t2 会先打印 from.num_things = 115,然后 to.numthings = 65。

然而,经过无数次的尝试,我总是得到相反的顺序。这就是我的困惑。

I expected the lock1 fill be unlocked first, and lock2 later.

不对,事实正好相反。在您的函数中,首先构建 lock1,然后构建 lock2。因此,当函数 returns lock2 先被销毁,然后 lock1,所以 lock2 的析构函数在 lock1 的析构函数之前释放它的锁。

std::lock 设法获取多个锁的实际顺序与锁如何被销毁和释放它们各自互斥锁的所有权无关。这仍然遵循正常的 C++ 规则。

say thread t1 runs first,

你无法保证这一点,无论如何。在上面的代码中,t2 完全有可能首先进入函数并获取互斥体上的锁。而且,完全有可能每次你 运行 这个程序你都会得到不同的结果,t1t2 随机赢得比赛。

在不进入技术 mumbo-jumbo 的情况下,C++ 唯一向您保证的是 std::thread 在线程函数在新的执行线程中被调用之前得到完全构建。你无法保证,当一个接一个地创建两个执行线程时,第一个执行线程将调用它的函数和 运行 线程函数的任意部分,然后第二个执行线程执行相同的操作。

所以 t2 完全有可能偶尔获得第一个锁。或者,总是。尝试跨执行线程控制事件的相对顺序比您想象的要难得多。