Home > Software engineering >  std::lock_guard and std::adopt_lock behaviour without locking the mutex
std::lock_guard and std::adopt_lock behaviour without locking the mutex

Time:10-28

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_locks 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.

  • Related