It smells so good! Finally know how to solve the c++ deadlock

Preface

Encountering a deadlock in the process of writing c/c++ often makes us feel free, because deadlocks are often not printed directly on the terminal like other types of errors, so it is difficult to be found and requires a lot of effort to troubleshoot. . It's time to think about how to prevent or avoid deadlock.

Deadlock scenario

  • Deadlock scenario 1

    • Unconsciously use too many locks in the class or globally, and do not notice the order of the locks when calling these locks in the function. For example, when one thread executes function fun1, the order of locks is A->B->C, and when another thread executes fun2, the order of locks is C->B->A. Unless it can be guaranteed that fun1 and fun2 will not be executed at the same time, and It is prone to deadlock.
  • Solution 1

    • If you use too many locks, there is probably a problem with the program design. Advice: Use as few locks as possible
    • ABC is locked sequentially, which means that the two mutexes are always locked in the same order: always lock the mutex A before the mutex B, and always lock the mutex BA before the mutex C, forever Will not deadlock.
  • Deadlock scenario 2 (as shown below)

    • In C++ swap two objects are locked in turn. On the
      surface, two lock barriers are used to lock in turn. There is no problem, but if we have one thread is swap(A,B), the other thread is swap at the same time. (B,A), imagine the consequences: one thread locks A.mutex and requests B.mutex, another thread locks B.mutex and requests A.mutex. Yes, there is a deadlock here.
      How can we solve this problem? ? ?
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
    
    
private:
 some_big_object some_detail;
 std::mutex m;
public:
 X(some_big_object const& sd):some_detail(sd){
    
    }

 friend void swap(X& lhs, X& rhs)
 {
    
    
   if(&lhs==&rhs)
     return;
   std::lock_guard<std::mutex> lock_a(lhs.m); // 2
   std::lock_guard<std::mutex> lock_b(rhs.m); // 3
   swap(lhs.some_detail,rhs.some_detail);
 }
};
  • Solution 2
    • std::lock——Multiple (more than two) mutexes can be locked at one time, and there are no side effects (deadlock risk). std::lockIf one lock cannot be successfully locked during the lock process, the function will unlock all locks. The internal is implemented with try_lock.
      You can see that the following call can solve the deadlock on swap. (adapt_lock is to let std::lock_guard own the lock and not lock it during construction, std::lock_guard unlocks the lock during destructuring after the function ends)
// 这里的std::lock()需要包含<mutex>头文件
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
    
    
private:
  some_big_object some_detail;
  std::mutex m;
public:
  X(some_big_object const& sd):some_detail(sd){
    
    }

  friend void swap(X& lhs, X& rhs)
  {
    
    
    if(&lhs==&rhs)
      return;
    std::lock(lhs.m,rhs.m); // 1
    std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // 2
    std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); // 3
    swap(lhs.some_detail,rhs.some_detail);
  }
};
  • Deadlock scenario 3 (as shown below)
    • Call the code provided by the user when the lock is held. The user requirement is to insert a number for a_simple_vector in this multi-thread, and this number does not exist in the vector it owns. Since it is necessary to check whether the vector has duplicate numbers during the insertion process (this is regarded as an interface prepared by external users), however, a_user_func is incorrectly used in the add_to_vec function, ha! It was deadlocked again, this time it was caused by locking it twice. (Cannot use nested lock)
class a_simple_vector {
    
    
public:
  void add_to_vec(int a) {
    
    
    std::lock_guard<std::mutex> lg(m);
    if (!a_user_func(a))
      v.push_back(a);
  }
  bool a_user_func(int a) {
    
    
    std::lock_guard<std::mutex> lg(m);
    for (auto &&i : v) {
    
    
      if (i == a) {
    
    
        printf("非法\n");
        return -1;
      }
    }
    return 0;
  }

private:
  std::mutex m;
  vector<int> v;
};
  • Solution 3
    • Do not use user codes or provide new methods: original user codes that are not locked, such as a_user_func_need_lock. This way, the lock will not be repeated.
class a_simple_vector {
    
    
public:
  void add_to_vec(int a) {
    
    
    std::lock_guard<std::mutex> lg(m);
    a_user_func_need_lock(a);
    v.push_back(a);
  }

private:
  bool a_user_func_need_lock(int a) {
    
    
    for (auto &&i : v) {
    
    
      if (i == a) {
    
    
        printf("非法\n");
        return -1;
      }
    }
    return 0;
  }
public:
  bool a_user_func(int a) {
    
    
    std::lock_guard<std::mutex> lg(m);
    for (auto &&i : v) {
    
    
      if (i == a) {
    
    
        printf("非法\n");
        return -1;
      }
    }
    return 0;
  }

private:
  std::mutex m;
  vector<int> v;
};
  • Deadlock scenario 4

    • To delete a node in a multi-threaded linked list, such as deleting B from the three ABC nodes, each node needs to maintain a lock, because we need to avoid the position of the current node and the previous and next nodes or their next, prev when deleting nodes The domain is modified, after getting the lock of B, we must lock AC. In the process of traversal, first lock A, and then lock B. This also causes deadlock.
  • Solution 4

    • Reused std::lock(),
      del node function in accordance with std::lock(b.prev.m,b.m)the order of the lock request, this time only during traversal from front to back, if you have a function Travel A lock, at this time can not get del A lock, or del simultaneously with two AB Lock, travel cannot get lock A. The same request sequence avoids the deadlock problem. It is logical to request the C lock after del has obtained the two locks. Of course, even if other threads have this C, as long as the interface is unified and locked from front to back, del or traval or insert Or push_back, there will be no deadlock waiting. (The code should actually be written and tried)
  • Deadlock scenario 5 (as shown below)

    • A list class and a request class can be thought of as a one-to-many scenario. To generate a Request object and add it to the Inventory object, you must first add Request.mutex and then Inventory.mutex, and
      Inventory traverses all Requests first. Inventory.mutex, then add Request.mutex, delete a Request object and delete it from the Inventory object. You must add Request.mutex first, then Inventory.mutex. It can be seen that the locking order of adding and deleting Request objects is opposite to the locking order of Inventory traversal printing each Request. Therefore, a deadlock may occur here.
  • Solution 5

    1. The lock and delete from g_inventory statements in the Request destructor switch positions, similar to the previous lock in the same order.
      The following two can be said to be adjusting the program structure to avoid deadlock.
    2. As shown in the figure below, a copy of requests is made in printAll, and the copy is locked and traversed to separate the process of locking Request and locking Inventory.
    3. Copy-on-write. The above statement is actually a kind of copy-on-read. Similarly,
      you can
      reset a new copy of the smart pointer of the container in the inventory when inserting or deleting the smart pointer. The reader printAll of the traversal container traverses in the old requests_ (by copying the local smart pointer), and the old requests_ is released when the local smart pointer is destroyed after the printAll ends. See the muduo library RequestInventory_test2.cc for details
class Inventory
{
    
    
 public:
  void add(Request* req)
  {
    
    
    muduo::MutexLockGuard lock(mutex_);
    requests_.insert(req);
  }
 
  void remove(Request* req) __attribute__ ((noinline))
  {
    
    
    muduo::MutexLockGuard lock(mutex_);
    requests_.erase(req);
  }
 
  void printAll() const;
 
 private:
  mutable muduo::MutexLock mutex_;
  std::set<Request*> requests_;
};
 
Inventory g_inventory;
class Request
{
    
    
 public:
  void process()            // __attribute__ ((noinline))
  {
    
    
    muduo::MutexLockGuard lock(mutex_);
    g_inventory.add(this);
    // ...
  }
 
  ~Request() __attribute__ ((noinline))
  {
    
    
    muduo::MutexLockGuard lock(mutex_);
    sleep(1);
    g_inventory.remove(this);
  }
 
  void print() const __attribute__ ((noinline))
  {
    
    
    muduo::MutexLockGuard lock(mutex_);
    // ...
  }
 
 private:
  mutable muduo::MutexLock mutex_;
};
```cpp
void Inventory::printAll() const
{
    
    
  muduo::MutexLockGuard lock(mutex_);
  sleep(1);
  for (std::set<Request*>::const_iterator it = requests_.begin(); it != requests_.end(); ++it)
  {
    
    
    (*it)->print();
  }
  printf("Inventory::printAll() unlocked\n");
}
void threadFunc()
{
    
    
  Request* req = new Request;
  req->process();
  delete req;       //~Request()
}
 
int main()
{
    
    
  muduo::Thread thread(threadFunc);
  thread.start();
  usleep(500 * 1000);
  g_inventory.printAll();
  thread.join();
}

solution

void Inventory::printAll() const
{
    
    
  std::set<Requests*>requests;
  {
    
    
  	muduo::MutexLockGuard lock(mutex_);
  	requests=requests_;
  }
  
  for (std::set<Request*>::const_iterator it = requests.begin(); it != requests.end(); ++it)
  {
    
    
    (*it)->print();
  }
  printf("Inventory::printAll() unlocked\n");
}

Hierarchical lock

As for how to lock in order, a hierarchical lock scheme is provided in "C++ Programming Practice": thread_local is used to isolate the default weight of the lock in each thread, and each time the lock is locked, the weight of the thread is gradually reduced. And to ensure that the weight of each lock is smaller than the last time, otherwise the way of throwing an exception, if we mistakenly add a higher weight than the current thread, the program will end abnormally first, instead of checking the deadlock When the task is delayed until the deadlock occurs, check it step by step.

#include <mutex>
#include <stdexcept>
#include <climits>

class hierarchical_mutex {
    
    
    std::mutex internal_mutex;
    unsigned long const hierarchy_value;
    unsigned long previous_hierarchy_value;
    static thread_local unsigned long this_thread_hierarchy_value;

    void check_for_hierarchy_violation() {
    
    
        if (this_thread_hierarchy_value <= hierarchy_value) {
    
    
            throw std::logic_error("mutex hierarchy violated");
        }
    }

    void update_hierarchy_value() {
    
    
        previous_hierarchy_value = this_thread_hierarchy_value;
        this_thread_hierarchy_value = hierarchy_value;
    }

public:
    explicit hierarchical_mutex(unsigned long value) :
            hierarchy_value(value),
            previous_hierarchy_value(0) {
    
    }

    void lock() {
    
    
        check_for_hierarchy_violation();
        internal_mutex.lock();
        update_hierarchy_value();
    }

    void unlock() {
    
    
        this_thread_hierarchy_value = previous_hierarchy_value;
        internal_mutex.unlock();
    }

    bool try_lock() {
    
    
        check_for_hierarchy_violation();
        if (!internal_mutex.try_lock())
            return false;
        update_hierarchy_value();
        return true;
    }
};

thread_local unsigned long
        hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);

int main() {
    
    
    hierarchical_mutex m1(42);
    hierarchical_mutex m2(2000);

}

to sum up

  1. Avoid nested locks (may hide some problems in the code. If you lock the outer function to perform traversal, lock the inner function and execute push_back will cause the iterator to fail, and this error can be exposed without using nested locks)
  2. Avoid calling user-supplied code while holding the lock
  3. Acquire locks using a fixed order
  4. Use lock hierarchy
Besides

Use trylock with caution to avoid program serialization (but you can use it to avoid some deadlocks)

That's all for today's deadlock, my friends three consecutive times!

reference

"C++ Programming Concurrent Practical Combat"
"Linux Multithreaded Server Programming"

Guess you like

Origin blog.csdn.net/adlatereturn/article/details/109137971