C++ multi-thread data sharing problem, mutex, deadlock and solution

1. The problem of data sharing between threads

Question: You share an apartment with your friend, and there is only one kitchen and one bathroom in the apartment. When your friend is in the bathroom, you will not be able to use it; or if two people book a ticket on the ticketing website, with the same seat, when one line of booking is operated, the other person can no longer operate or even operate. Success, otherwise there will be a lot of trouble.

The same problem, for multi-threading, there are the following situations:

1. When two threads access different memory locations, there will be no problem, which is equivalent to that you and your friends do not share the rent and live separately;

2. When two threads operate on shared data, if they just read together, there will be no problem;

3. But if one thread wants to read and another thread wants to write, there will be problems, so protecting shared data needs to be considered in multithreading.

The following code does not consider the data sharing problem between the data reading thread and the data writing thread, which will cause the problem just analyzed and report an error.

#include <iostream>
#include <thread>
#include <list>
using namespace std;

class OperateData{
public:
\t//data input
void writeData()
{
for (int i = 0; i < 10000; i ++ )
{
cout << "write data" << i << endl;
dataList. push_back(i);
}
}
// read data
void readData()
{
for (int i = 0; i < 10000; i ++ )
{
if (!dataList.empty())//data is not empty
{
int data = dataList.front();//returns the first element, but does not check whether the element exists
dataList.pop_front();//Remove the first element, but do not return
}
else
{
// data is empty
cout << "The data is empty"<<endl;
}
}
cout << "end" << endl;
}
private:
list<int> dataList;
};
int main()
{
OperateData myobj;
thread readObj( & amp;OperateData::readData, & amp;myobj);//The second parameter is a reference to ensure that the same object is used in the thread
thread writeObj( &OperateData::writeData, &myobj);
readObj. join();
writeObj. join();
}

2. Methods of protecting shared data

Use a mutex to protect shared data

Before accessing shared data, lock the data, and unlock the data after the access. The thread library needs to ensure that when a thread uses a specific mutex to lock shared data, other threads that want to access the locked data must wait until the previous thread unlocks the data before they can access it.

Mutexes have their own problems, causing deadlocks, or too much (or too little) data protection.

First you need to import the header file:

#include <mutex>

In C++, create a mutex instance by instantiating std::mutex, lock the mutex through the member function lock(), and unlock()
to unlock. That is:

lock()–>operate shared data–>unlock()

lock() and unlock() must be used in pairs!

#include 
#include 
#include 
#include <mutex>
using namespace std;

class OperateData{
public:
\t//data input
void writeData()
{
for (int i = 0; i < 100000; i ++ )
{
cout << "write data" << i << endl;
my_mutex. lock();
dataList. push_back(i);
my_mutex. unlock();
}
}
// read data
void readData()
{
for (int i = 0; i < 100000; i ++ )
{
my_mutex. lock();
if (!dataList.empty())//data is not empty
{
int data = dataList.front();//returns the first element, but does not check whether the element exists
dataList.pop_front();//Remove the first element, but do not return
}
else
{
// data is empty
cout << "The data is empty"< dataList;
mutex my_mutex;
};
int main()
{
OperateData myobj;
thread readObj( & amp;OperateData::readData, & amp;myobj);//The second parameter is a reference to ensure that the same object is used in the thread
thread writeObj( &OperateData::writeData, &myobj);
readObj. join();
writeObj. join();

return 0;
}

The modified program can run stably.

Because it is easy to miss when used in pairs, the C++ standard library provides a template class std::lock_guard for mutexes, which can provide locked (execute lock) mutexes at construction time , and unlock (execute unlock) when destructing, thus ensuring that a locked mutex can be unlocked correctly. Lock() and unlock() cannot be used when lock_guard is used.

std::lock_guard<std::mutex> guard(my_mutex);
//write data
void writeData()
{
for (int i = 0; i < 100000; i ++ )
{
cout << "write data" << i << endl;
lock_guard<std::mutex> guard(my_mutex);
dataList. push_back(i);
}
}
// read data
void readData()
{
for (int i = 0; i < 100000; i ++ )
{
lock_guard<std::mutex> guard(my_mutex);
if (!dataList.empty())//data is not empty
{
int data = dataList.front();//returns the first element, but does not check whether the element exists
dataList.pop_front();//Remove the first element, but do not return
}
else
{
// data is empty
cout << "The data is empty"<<endl;
}
}
cout << "end" << endl;
}

3. Deadlock

A pair of threads need to do something with all their mutexes, where each thread has a mutex and waits for the other to unlock it. This way no thread can do work, because they are all waiting for each other to release the mutex. This situation is a deadlock, and its biggest problem is that two or more mutexes lock an operation.

The prerequisite for deadlock is that there are at least two mutexes

Two threads A, B

(1) Thread A executes, mutex A locks, and then it is the turn of mutex B to lock

(2) A context switch happens to occur at this time

(3) Thread B executes, mutex B is locked, and then it is the turn of mutex A to lock, and then…it is found that A has been locked

(4) Then there is a deadlock

The code where the interlock situation occurs:

#include 
#include 
#include 
#include <mutex>
using namespace std;

class OperateData{
public:
\t//data input
void writeData()
{
for (int i = 0; i < 100000; i ++ )
{
cout << "write data" << i << endl;
my_mutex_1.lock(); //First lock 1 and then lock 2
            my_mutex_2. lock();
dataList. push_back(i);
my_mutex_2. unlock();
            my_mutex_1.unlock(); //The order of unlocking does not matter
}
}
// read data
void readData()
{
for (int i = 0; i < 100000; i ++ )
{
my_mutex_2.lock(); //First lock 2 and then lock 1
            my_mutex_1. lock();
if (!dataList.empty())//data is not empty
{
int data = dataList.front();//returns the first element, but does not check whether the element exists
dataList.pop_front();//Remove the first element, but do not return
}
else
{
// data is empty
cout << "The data is empty"< dataList;
mutex my_mutex_1;
mutex my_mutex_2;
};
int main()
{
OperateData myobj;
thread readObj( & amp;OperateData::readData, & amp;myobj);//The second parameter is a reference to ensure that the same object is used in the thread
thread writeObj( &OperateData::writeData, &myobj);
readObj. join();
writeObj. join();

return 0;
}

Four. Deadlock solution

1. As long as the order of locking the two mutexes is consistent, there will be no deadlock, and the lock_guard is the same;

std::lock_guard<std::mutex> guard(my_mutex_1);

std::lock_guard<std::mutex> guard(my_mutex_2);

2. There is also a risk of deadlock in the above situation. The C ++ standard library has a way to solve this problem. std::lock() – can lock multiple (more than two) locks at one time Mutex, and no side effects (deadlock risk). When std::lock successfully acquires a lock on a mutex, and when it tries to acquire a lock from another mutex, an exception will be thrown, and the first lock will also be generated with an exception And automatic release, so std::lock either locks both locks, or neither locks.

std::lock(my_mutex_1, my_mutex_2);

//....

my_mutex_1. unlock();

my_mutex_2. unlock();

3. Using std::lock() is easy to forget to use unlock(). For this situation, C++17 provides support for this situation. std::scoped_lock<> A new The RAII type template type of std::lock_guard<> is functionally equivalent. This new type can accept an indefinite number of mutex types as template parameters, and the corresponding mutexes (number and type) as construction parameters. The mutex supports construction and locking, which is the same as the usage of std::lock, and its unlocking phase is carried out in the destruction.

std::scoped_lock<std::mutex,std::mutex> guard(lhs.m,rhs.m);

4. Similarly, std::lock() can also be used in conjunction with std::lock_guard<>, but std::lock_guard<> has already used std::lock() by default in the construction, so the parameter Setting std::adopt_lock in avoids calling lock() in the constructor.

std::lock(my_mutex_1, my_mutex_2);

//....

std::lock_guard<std::mutex> guard_1(my_mutex_1, std::adopt_lock);

std::lock_guard<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

5. Avoid nested locks: When a thread has acquired a lock, don’t acquire the second one. Because each thread holds only one lock, there is no deadlock on the lock.

5.std::unique_lock

std::unique_lock has a similar function to std::lock_guard<>, and can be used instead when there are no parameters, but std::unique_lock is more flexible and can be adapted with more parameters, but std::unique_lock will take up more space, and slightly slower than std::lock_guard.

1. The parameter std::adopt_lock

Like the above use in std::lock_guard<>, std::lock() and std::lock_guard<> need to be used together

std::lock(my_mutex_1, my_mutex_2);

//....

std::unique_lock<std::mutex> guard_1(my_mutex_1, std::adopt_lock);

std::unique_lock<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

2. The parameter std::try_to_lock

Cannot be used with lock at the same time, try to lock the mutex with lock() of mutex, but if the lock is not successful, it will return immediately without blocking there; the reason for using try_to_lock is to prevent other threads from locking the mutex for too long, causing This thread has been blocked in the lock. Use owns_lock() to determine whether to get the lock.

std::unique_lock<std::mutex> guard(my_mutex,std::try_to_lock);
if(guard. owns_lock())
{
    // got the lock
    dataList. push_back(i);
}
else
{
    // did not get the lock
    cout<<"Did not get the lock"<<endl;
}

3. Parameter std::defer_lock

A mutex that is not locked is initialized. The purpose of not locking it is that some methods of unique_lock can be called in the future, and it cannot be locked in advance.

4.std::unique_lock member function

  • lock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock(); //Auto unlock()
  • unlock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock. lock();
//...handle the shared data
myUniLock.unlock(); //Temporarily unlock
//...handle non-shared data
myUniLock. lock();
  • try_lock()
std::unique_lock<std::mutex> guard(my_mutex,std::defer_lock);
if(guard. try_lock() == true)
{
    // got the lock
    dataList. push_back(i);
}
else
{
    // did not get the lock
    cout<<"Did not get the lock"<<endl;
}
  • release()

std::unique_lock myUniLock(myMutex); is equivalent to binding myMutex and myUniLock together, release() is to unbind, return the pointer of the mutex object it manages, and release the ownership.

std::unique_lock<std::mutex> myUniLock(my_mutex);
std::mutex* ptx = myUniLock.release();//Unbind my_mutex and myUniLock
//...operate on shared data
ptx->unlock(); //Need to unlock manually

Six. Lock granularity

The lock granularity is used to describe the amount of data protected by a lock. A fine-grained lock (a fine-grained lock) can protect a smaller amount of data, and a coarse-grained lock (a coarse-grained lock) can protect a larger amount of data. Choose a lock with an appropriate granularity.

Seven.Transfer of ownership of unique_lock

Copying Ownership Is Illegal

std::unique_lock<std:mutex> myUniLock_1(mutex);
std::unique_lock<std:mutex> myUniLock_2(myUniLock_1);

Need to move ownership

std::unique_lock<std:mutex> myUniLock_1(mutex);
std::unique_lock<std:mutex> myUniLock_2(std::move(myUniLock_1)); //now myUniLock_1 points to null, myUniLock_2 points to mutex
std::unique_lock<std:mutex> lk()
{
    std::unique_lock<std:mutex> tempUniLock(mutex);
    return tempUniLock;//move constructor
}
// Then it can be called in the outer layer, and the guard has ownership of the mutex
std::unique_lock<std::mutex> guard = lk();