I have been learning about the usage of std::lock
and std::lock_guard
and most examples follow the pattern below:
std::lock(m1, m2);
std::lock_guard<std::mutex> guard1(m1, std::adopt_lock);
std::lock_guard<std::mutex> guard2(m2, std::adopt_lock);
//Do something here
Then I came across an example that utilized the same pattern you would use if you were using std::unique_lock
, but with lock_guard
:
std::lock_guard<std::mutex> guard1(m1, std::adopt_lock);
std::lock_guard<std::mutex> guard2(m2, std::adopt_lock);
std::lock(m1, m2);
//Do something here
My question is, would this cause undefined behaviour if you use the second pattern and an exception occurs before you reach std::lock
?
P.S. I am aware that C 17 introduced std::scoped_lock
and that std::lock_guard
is still around mainly for compatibility with older code.
CodePudding user response:
Your second example is undefined behavior; adopt_lock
constructor presumes that the mutex is already held. This UB is triggered at construction, not at destruction or when an exception is thrown.
If you used unique_lock
instead of scoped_lock
, it has a:
unique_lock( mutex_type& m, std::defer_lock_t t ) noexcept;
constructor, which permits your std::lock
use with a slight change:
std::unique_lock<std::mutex> guard1(m1, std::defer_lock);
std::unique_lock<std::mutex> guard2(m2, std::defer_lock);
std::lock(guard1, guard2);
Now, unique_lock
does track if it has the lock, so there is possible memory and performance overhead; if unique_lock
s are local to the scope, compilers can and will optimize out that if it can prove it safe.
And if the compiler can't prove it safe, quite often it isn't safe.