educative.io

Second example to resolve deadlock seems incorrect

I think in the second example of deadlockResolved.cpp, lock(a.mut, b.mut) should come at the starting of the function deadLock(CriticalData& a, CriticalData& b) and after that it should be adopted. Please clarify.


Course: https://www.educative.io/courses/concurrency-with-modern-cpp
Lesson: https://www.educative.io/courses/concurrency-with-modern-cpp/gxK2oG03omj

Hi @Aditya_Sharma !!

// deadlockResolved.cpp

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>

using namespace std;

struct CriticalData{
  mutex mut;
};

void deadLock(CriticalData& a, CriticalData& b){

  lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
  cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;

  this_thread::sleep_for(chrono::milliseconds(1));

  lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);
  cout << "  Thread: " << this_thread::get_id() << " second mutex" <<  endl;

  cout << "    Thread: " << this_thread::get_id() << " get both mutex" << endl;
  lock(a.mut, b.mut);
  // do something with a and b
}

int main(){

  cout << endl;

  CriticalData c1;
  CriticalData c2;

  thread t1([&]{deadLock(c1,c2);});
  thread t2([&]{deadLock(c2,c1);});

  t1.join();
  t2.join();

  cout << endl;

}

In the provided example deadlockResolved.cpp, the usage of lock(a.mut, b.mut) is not placed at the beginning of the deadLock function because lock_guard is used instead of unique_lock.

When lock_guard is used, it’s typically constructed with a locked mutex. Therefore, you cannot call lock(a.mut, b.mut) at the beginning of the function because it would attempt to lock the mutexes again, leading to a potential deadlock.

Let’s clarify the sequence of events in the deadLock function:

  1. lock_guard instances guard1 and guard2 are constructed with std::adopt_lock, indicating that the mutexes are already locked.
  2. The output indicating the acquisition of the first mutex (guard1) is printed.
  3. A delay of 1 millisecond is introduced.
  4. The output indicating the acquisition of the second mutex (guard2) is printed.
  5. The output indicating that both mutexes are acquired is printed.
  6. lock(a.mut, b.mut) is called to ensure atomic locking of both mutexes. This is necessary to avoid potential deadlock situations.

Therefore, in this example, the placement of lock(a.mut, b.mut) is correct and should not be moved to the beginning of the function. It ensures that both mutexes are locked atomically and in a consistent order, which is essential for preventing deadlocks.
I hope it helps. Happy Learning :blush:

I found this example C++11 Locking Strategy: adopt_lock and defer_lock | by Asit Dhal | Medium where they used lock_guard with adopt_lock. They used unique_lock with defer_lock.

I ran the code with lock(a.mut, b.mut) at beginning and lock_guardstd::mutex guard1(a.mut, std::adopt_lock) on my local machine but it ran without issue.

Also why unique_lock is used with defer_lock?