1

我有以下代码,来自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 完成代码的关键部分,包装器 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。

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

在此处输入图像描述

4

1 回答 1

1

我希望先解锁 lock1 填充,然后再解锁 lock2。

不,反之亦然。在您的函数lock1中首先构造,然后lock2. 因此,当函数返回lock2时首先被销毁,那么lock1,的析构函数在 ' 的析构函数lock2之前释放它的锁lock1

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

说线程 t1 首先运行,

无论如何,您对此没有任何保证。在上面的代码中,完全有可能t2首先进入函数并获取互斥锁上的锁。而且,完全有可能每次运行这个程序时,你都会得到不同的结果,两者都是t1随机t2的。

在不涉及技术性的大问题的情况下,C++ 向您保证的唯一一件事是std::thread在线程函数在新的执行线程中被调用之前完全构造。您无法保证,当一个接一个地创建两个执行线程时,第一个将调用其函数并在第二个执行线程执行相同操作之前运行线程函数的任意部分。

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

于 2020-10-04T15:24:45.153 回答