что означает unique_lock, когда один поток получает 2 unique_lock одного и того же мьютекса?

У меня есть следующий код, взятый из 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 с двумя блокировками. Как только блокировки заблокированы, и можно присвоить num_things значение num=10
  3. по завершении он будет печатать from.num_things = 110 и to.numthings = 60 по порядку. сначала "от", потом "к" позже.
  4. thread1 завершает критическую часть кода, а оболочка unique_lock вызывает свой деструктор, который в основном разблокирует мьютекс.

Вот чего я не понимаю.

Я ожидал, что сначала будет разблокирована заливка lock1, а позже - lock2. Затем поток t2 получает мьютекс в том же порядке и сначала блокирует lock1, затем lock2. Он также последовательно запускает критический код до cout.

Поток t2 примет глобальные acc1.num_things = 110 и acc2.num_things = 60 от t1.

Я ожидаю, что t2 сначала напечатает from.num_things = 115, а затем to.numthings = 65.

Однако после бесчисленных испытаний я всегда получаю обратный порядок. И в этом мое замешательство.

1 ответ

Решение

Я ожидал, что сначала будет разблокирована заливка lock1, а позже - lock2.

Нет, верно обратное. В вашей функции lock1 сначала создается, затем lock2. Следовательно, когда функция возвращает lock2 сначала уничтожается, затем lock1, так lock2деструктор снимает блокировку перед lock1деструктор.

Фактический порядок, в котором std::lockудается получить несколько блокировок, не имеет никакого отношения к тому, как блокировки будут уничтожены, и освободить их владение соответствующими мьютексами. Это по-прежнему соответствует обычным правилам C++.

скажем, поток t1 запускается первым,

У вас нет никаких гарантий в этом. В приведенном выше коде вполне возможно, что t2сначала войдет в функцию и получит блокировки мьютексов. И также вполне возможно, что каждый раз, когда вы запускаете эту программу, вы будете получать разные результаты, причем оба t1 и t2 выиграть гонку случайным образом.

Не вдаваясь в техническую чепуху, единственное, что вам гарантирует C++, это то, что std::threadполностью создается до того, как функция потока будет вызвана в новом потоке выполнения. У вас нет никаких гарантий, что при создании двух потоков выполнения один за другим первый вызовет свою функцию и выполнит некоторую произвольную часть функции потока до того, как второй поток выполнения сделает то же самое.

Так что вполне возможно, что t2время от времени попадет в замки первыми. Или всегда. Попытка контролировать относительную последовательность событий в потоках выполнения намного сложнее, чем вы думаете.

Другие вопросы по тегам