current position:Home>C + + parallel programming (4)

C + + parallel programming (4)

2022-05-15 05:17:21Robot Laboratory of Hefei University of Technology

Data sharing between processes

Reference blog

Sharing data between threads —— Protect shared data with mutex

[c++11] Multithreaded programming ( Four )—— Deadlock (Dead Lock)

c****++ Deadlock in multithreading ****

C++ Deadlock and solutions

The problem of sharing data

Imagine sharing an apartment with a friend for a while , The apartment has only one kitchen and one bathroom . Unless your feelings are particularly deep , Otherwise, it is impossible to use the bathroom at the same time . in addition , If a friend occupies the bathroom for a long time , And you happen to need , Will feel inconvenient . Similarly , Suppose you use a combined oven , Although you can cook at the same time , But if one person wants to bake sausage , At the same time, another person wants to bake a cake , The result should not be very good . also , We also know the trouble of sharing office space : Things haven't been done yet , Someone borrowed what they needed for their work , Or the semi-finished products are changed by others without authorization

The same is true for threads . If data is shared between threads , We need to follow the norms : Which thread accesses what data in what way ; also , Once the data is changed , If other threads are involved , When and how they want to be notified . Between multiple threads in the same process , Although it is easy to share data , But this is not an absolute advantage , Sometimes it's even a big disadvantage . Incorrect use of shared data , Is a big inducement for concurrency related errors , The consequences are far greater than “ Sausage flavored cake ” serious

Vicious conditional competition

The typical scenario of inducing vicious conditional competition is , To complete an operation , But you need to change two or more different data , Like the two link pointers in the above example . Because the operation involves two separate pieces of data , And they can only be changed with separate instructions , When one of the data changes , Other threads may visit unexpectedly . Because the time window to meet the conditions is short , Therefore, conditional competition is often difficult to detect and reproduce . If the change operation is carried out continuously CPU Command completed , There is less chance of causing problems in any single run , Even if other threads are accessing data concurrently . Only executing instructions in some order can cause problems . As the system load increases and the number of operations increases , The chances of this order will also increase .“ It rains all night when the house leaks ” Almost inevitable , And these problems will happen under the most untimely circumstances . Vicious conditional competition is common “ Fastidious ” Timing of emergence , When the application is running in a debug environment , They often disappear completely , Because debugging tools affect the internal execution timing of the program , Even if it only affects a little

Protect shared data with mutex

Before accessing shared data , Developers can use mutex to lock relevant data , And unlock the data after the access . therefore , The thread library needs to ensure that when a thread locks the shared data with a specific mutex , Other threads can only access after the data is unlocked

lock()、 unlock() Lock and unlock

C++ By instantiating std::mutex Create mutexes , By calling member functions lock() To lock ,unlock() To unlock

In practice, it must be used in pairs , Once you use... In a function lock, Must be called at the function exit unlock

mutex my_mutex;
int a = 1;
bool func()
{
       
    my_mutex.lock();
    if(!a)
    {
    
        cout << "a = " << a << endl;
        my_mutex.unlock();
        return false;
    }
    my_mutex.unlock();
    return true;    
}

Note that the above code is in return The previous calls unlock

RAII std::lock_guard

C++ The standard library provides a RAII Template class for Syntax std::lock_guard, It will be in ** Provide locked mutex when constructing , And unlock it during deconstruction **, This ensures that a locked mutex will always be unlocked correctly

mutex my_mutex;
int a = 1;
bool func()
{
       
    lock_guard<mutex> my_guard(my_mutex);
    // my_mutex.lock();
    if(!a)
    {
    
        cout << "a = " << a << endl;
        // my_mutex.unlock();
        return false;
    }
    // my_mutex.unlock();
    return true;    
}

You can limit lock_guard To release the lock in advance

mutex my_mutex;
int a = 1;
bool func()
{
    
    // lock_guard<mutex> my_guard(my_mutex);
    // my_mutex.lock();
    if (!a)
    {
    
        {
    
            lock_guard<mutex> my_guard(my_mutex);
            cout << "a = " << a << endl;
        }
        // my_mutex.unlock();
        return false;
    }
    // my_mutex.unlock();
    return true;
}

Object oriented design criteria : Treat mutex as data member Place in class

It should be noted that , The mutex and the data to be protected need to be defined as private member , All member functions need to lock the data when calling , Unlock the data at the end , In this way, it can ensure that the data is not destroyed

The reality is not always so ideal , Be aware of , If a member function returns a pointer or reference that protects data , Then there must be the possibility of data destruction , The reason lies in Users can access data directly through references or pointers , So as to bypass the protection of mutex . therefore , If a class uses mutexes to protect its data members , Its developers must Carefully design the interface , Make sure the mutex locks any access to the data , And leave no back door

class some_data
{
    
  int a;
  std::string b;
public:
  void do_something();
};

class data_wrapper
{
    
private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  {
    
    std::lock_guard<std::mutex> l(m);
    func(data);    // 1  Pass on “ Protect ” Data to user function 
  }
};

some_data* unprotected;

void malicious_function(some_data& protected_data)
{
    
  unprotected=&protected_data;
}

data_wrapper x;
void foo()
{
    
  x.process_data(malicious_function);    // 2  Pass a malicious function 
  unprotected->do_something();    // 3  Access protected data without protection 
}

look process_data No problem , But call user-defined func Means that the foo You can bypass the protection mechanism and put the function malicious_function Pass in , Call... Without mutex locking do_something

C++ The standard library does not protect against this behavior , Therefore, it must be borne in mind : Do not pass pointers or references to protected data outside the scope of the mutex

Deadlock

First, take a basic example to abstract what is deadlock :

interviewer :“ If you can make it clear what a deadlock is , I'll send you offer”
The candidate :" If you can send me offer, I'll tell you what a deadlock is ”

Deadlock is such a scenario : There is a pair of threads , They all need to do something , These operations begin with locking their mutexes , And the other party needs to release the mutex it holds , In this scenario, no thread can work properly , Because they are waiting for each other to release the mutex . When more than two mutexes lock the same operation , Deadlocks are easy to happen

Take a chestnut , If there is a thread at this time A, Lock it first a, Relock b, And there's another thread at the same time B, Lock it first b Relock a The order to get the lock . As shown in the figure below :

 Insert picture description here

As shown in the figure, in this case , Threads A Waiting for the lock b, But lock b Locked in , So you can't go down at this time , Need to wait for the lock b Release , And threads B First locked the lock b, Waiting for the lock a Release , This creates threads A Wait for the thread B, Threads B Wait for thread A, So there's a deadlock

#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'll find the program stuck , This is the life and death lock . When the program runs, something like the following may happen :

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

Common deadlock situations

1、 Forget to release the lock

mutex _mutex;
void func()
{
    
	_mutex.lock();
	if (xxx)
	  return;
	_mutex.unlock();
}

2、 A single thread repeatedly applies for a lock

mutex _mutex;
void func()
{
    
	_mutex.lock();
	 //do somrthing....
	_mutex.unlock();
}
 
void data_process() {
    
	_mutex.lock();
	func();
	_mutex.unlock();
}

3、 Dual thread multi lock application

mutex _mutex1;
mutex _mutex2;
 
void process1() {
    
	_mutex1.lock();
	_mutex2.lock();
	//do something1...
	_mutex2.unlock();
	_mutex1.unlock();
}
 
void process2() {
    
	_mutex2.lock();
	_mutex1.lock();
	//do something2...
	_mutex1.unlock();
	_mutex2.unlock();
}

4、 Ring lock application

/* * A - B * | | * C - D */

The solution to deadlock

1、 You can compare mutex The address of , Lock the small address first every time

If the hard condition constraints, we have to acquire multiple locks and can't use std::lock, Then the guideline is to ensure that these locks are obtained in the same order in each thread

if(&_mu < &_mu2){
    
    _mu.lock();
    _mu2.unlock();
}
else {
    
    _mu2.lock();
    _mu.lock();
}

2、 Try 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;
}

3、 Do not use user-defined code in mutex protected areas , Because the user's code may operate on other mutexes

{
    
 std::lock_guard<std::mutex> guard(_mu2);
 user_function(); // never do this!!!
    f << msg << id << endl;
}

4、 If you want to lock multiple mutexes at the same time , To use std::lock()

std::lock(_mu, _mu2);

5、 Use hierarchical locks

Use hierarchical locks , Wrap the mutex , Define a hierarchy of properties for locks , Lock each time according to the order from high to low

When the code attempts to perform a lock operation , It checks whether the lock from the lower level is currently held , If so, it is forbidden to lock the current mutex

hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);

int do_low_level_stuff();

int low_level_func() {
    
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex);
  return do_low_level_stuff(); 
}

void high_level_stuff(int some_param);

void high_level_func() {
    
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex);
  high_level_stuff(low_level_func());
}

void thread_a() {
    
  high_level_func(); 
}

hierarchical_mutex other_mutex(100); 
void do_other_stuff();

void other_stuff() {
    
  high_level_func();
  do_other_stuff(); 
}

void thread_b() {
    
  std::lock_guard<hierarchical_mutex> lk(other_mutex);
  other_stuff(); 
}

thread_a The hierarchy rules are followed , and thread_b No, . It can be noted that ,thread_a Called high_level_func, Therefore, high-level mutex high_level_mutex Locked , Then he tried to call low_level_func, At this time, the low-level mutex low_level_mutex Locked , This is consistent with the rules mentioned above : Lock the high level first and then the low level

thread_b The operation of is not so optimistic , It's locked first. The level is 100 Of other_mutex, And then try to lock the high-level high_level_mutex, There will be an error , An exception may be thrown , Or directly terminate the program

copyright notice
author[Robot Laboratory of Hefei University of Technology],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/131/202205111245214908.html

Random recommended