I have the following code, which is from https://en.cppreference.com/w/cpp/thread/unique_lock. However, upon, printing the output, I see some unexpected result and would like some explaination.
The code is:
#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();
}
My expectation:
Here is what I don't understand.
I expected the lock1 fill be unlocked first, and lock2 later. Thread t2 then acquire the mutex in the same order and lock the lock1 first, then lock2. It will also runs the critical code sequentially up to cout.
Thread t2 will take the global acc1.num_things = 110 and acc2.num_things = 60 from t1.
I expect that t2 will print from.num_things = 115 first, then to.numthings = 65.
However, upon countless trial, I always get the reverse order. And that is my confusion.
I expected the lock1 fill be unlocked first, and lock2 later.
No, the reverse is true. In your function lock1
gets constructed first, then lock2
. Therefore, when the function returns lock2
gets destroyed first, then lock1
, so lock2
's destructor releases its lock before lock1
's destructor.
The actual order in which std::lock
manages to acquire the multiple locks has no bearing on how the locks gets destroyed, and release their ownership of their respective mutexes. That still follows normal C++ rules for doing so.
say thread t1 runs first,
You have no guarantee of that, whatsoever. In the above code it's entirely possible that t2
will enter the function first and acquire the locks on the mutexes. And, it is also entirely possible that each time you run this program you'll get different results, with both t1
and t2
winning the race, randomly.
Without getting into technical mumbo-jumbo, the only thing that C++ guarantees you is that std::thread
gets fully constructed before the thread function gets invoked in a new execution thread. You have no guarantees whatsoever that, when creating two execution threads one after another, the first one will call its function and run some arbitrary part of the thread function before the second execution thread does the same.
So it's entirely possible that t2
will get the first dibs on the locks occasionally. Or, always. Attempting to control the relative sequence of events across execution threads is much harder than you think.