что означает 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();
}
Мое ожидание:
- acc1 будет инициализирован с num_things=100, а acc2 с num_things=50.
- скажем, поток t1 запускается первым, он получает мьютекс m с двумя блокировками. Как только блокировки заблокированы, и можно присвоить num_things значение num=10
- по завершении он будет печатать from.num_things = 110 и to.numthings = 60 по порядку. сначала "от", потом "к" позже.
- 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
время от времени попадет в замки первыми. Или всегда. Попытка контролировать относительную последовательность событий в потоках выполнения намного сложнее, чем вы думаете.