c++11 multithreaded programming (IV) -- dead lock

Posted by betman_kizo on Wed, 01 Dec 2021 19:58:23 +0100

deadlock

If you lock a mutex but do not release it, a deadlock will occur when another thread accesses the resources protected by the lock. In this case, use lock_guard can ensure that the lock can be released when destructing. However, when an operation needs to use two mutually exclusive elements, only lock is used_ Guard does not guarantee that deadlock will not occur, as shown in the following example:

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;

class LogFile {
    std::mutex _mu;
    std::mutex _mu2;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {
        std::lock_guard<std::mutex> guard(_mu);
        std::lock_guard<std::mutex> guard2(_mu2);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
    void shared_print2(string msg, int id) {
        std::lock_guard<std::mutex> guard(_mu2);
        std::lock_guard<std::mutex> guard2(_mu);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
};

void function_1(LogFile& log) {
    for(int i=0; i>-100; i--)
        log.shared_print2(string("From t1: "), i);
}

int main()
{
    LogFile log;
    std::thread t1(function_1, std::ref(log));

    for(int i=0; i<100; i++)
        log.shared_print(string("From main: "), i);

    t1.join();
    return 0;
}

After running, you will find that the program will get stuck, which is a life and death lock. The following situations may occur when the program runs:

Thread A              Thread B
_mu.lock()          _mu2.lock()
   //Deadlock / / deadlock
_mu2.lock()         _mu.lock()

There are many solutions:

  1. You can compare the addresses of mutex. Lock the smaller address each time, such as:

    if(&_mu < &_mu2){
        _mu.lock();
        _mu2.unlock();
    }
    else {
        _mu2.lock();
        _mu.lock();
    }
    
  2. Using hierarchical lock, wrap the mutex lock, define a hierarchical attribute for the lock, and lock it from high to low each time.

In fact, both methods strictly stipulate the locking sequence, but the implementation methods are different.

The std::lock() function is provided in the c + + standard library to ensure that multiple mutually exclusive locks are locked at the same time,

std::lock(_mu, _mu2);

At the same time, lock_ The guard also needs to be modified, because the mutex lock has been locked, then lock_ The guard should not be locked when it is constructed, but the lock should be released when it is destructed. Use std::adopt_lock means no locking is required:

std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);

The complete code is as follows:

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;

class LogFile {
    std::mutex _mu;
    std::mutex _mu2;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {
        std::lock(_mu, _mu2);
        std::lock_guard<std::mutex> guard(_mu, std::adopt_lock);
        std::lock_guard<std::mutex> guard2(_mu2, std::adopt_lock);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
    void shared_print2(string msg, int id) {
        std::lock(_mu, _mu2);
        std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
        std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);
        f << msg << id << endl;
        cout << msg << id << endl;
    }
};

void function_1(LogFile& log) {
    for(int i=0; i>-100; i--)
        log.shared_print2(string("From t1: "), i);
}

int main()
{
    LogFile log;
    std::thread t1(function_1, std::ref(log));

    for(int i=0; i<100; i++)
        log.shared_print(string("From main: "), i);

    t1.join();
    return 0;
}

To sum up, the following suggestions are made to avoid deadlock:

  1. It is recommended to lock only one mutex at the same time.

    {
     std::lock_guard<std::mutex> guard(_mu2);
     //do something
        f << msg << id << endl;
    }
    {
     std::lock_guard<std::mutex> guard2(_mu);
     cout << msg << id << endl;
    }
    
  2. Do not use user-defined code in the mutex protected area, because the user's code may operate other mutexes.

    {
     std::lock_guard<std::mutex> guard(_mu2);
     user_function(); // never do this!!!
        f << msg << id << endl;
    }
    
  3. If you want to lock multiple mutexes at the same time, use std::lock().

  4. Define the order of locks (use hierarchical locks, or compare addresses, etc.) and lock them in the same order each time. See the detailed introduction C + + concurrent programming practice.

reference resources

  1. C + + concurrent programming practice
  2. C++ Threading #4: Deadlock

Topics: C++