Multithreading 04: mutexes and deadlocks

📕 Mutex and deadlock

1, Data sharing issues

First, let's look at the execution order of multithreading:

void TextThread() {
	cout << "I am a thread:" << this_thread::get_id() << endl;
	//In thread operation code
	cout << "thread " << this_thread::get_id() << "End of operation" << endl;
}

int main()
{
	vector<thread> threadVec;
	for (int i = 0; i < 5; ++i) {
		threadVec.push_back(thread(TextThread));
	}
	for (int i = 0; i < 5; ++i) {
		threadVec[i].join();
	}
	return 0;
}

  • Putting the thread object into the container for management looks like an array of Thread objects, which is good for creating a large number of threads at one time and managing a large number of threads;
  • The execution order of multiple threads is chaotic, which is related to the operation scheduling mechanism of threads within the operating system;

However, the above threads do not involve the communication between threads. What happens if multiple threads are involved in operating the same pile of data? (everyone who has studied the operating system knows that this is the problem of data sharing)

  1. If it is thread read-only data, it is safe and stable, and there will be no problem;
  2. If there is reading and writing: ① there will be execution errors without processing, such as reading and writing the same data at the same time, such as a simple + + i, with three or four assembly codes at the bottom. During the running process (before the + + i operation is completed), the threads are scheduled, while other threads use this i value, which is + + i, resulting in value confusion!; ② The simplest way to prevent crash: you can't write when you read, and you can't read when you write.

2, Mutex

2.1 basic concept of mutex

  • In c + +, mutex is a class object, which can be understood as a lock. Multiple threads try to lock with lock() member function, and only one thread can lock successfully. If the lock fails, the process will be stuck in lock() and continue to try to lock.
  • Be careful with the use of mutex, and protect no more or less data. Less will not achieve the effect, and more will affect the efficiency.

2.2 usage of mutex

Header file contains #include < mutex >

Most basic usage

①lock(),unlock()

  • Step: 1 lock(); 2. Operate shared data; 3.unlock();

  • lock() and unlock() should be used in pairs. Note this:

    //Function to fetch data
    bool outMsgPro(int& command) {
    
    	myMutex.lock();
    	if (!msgRecvQueue.empty()) {//Operate if it is not empty
    		int command = msgRecvQueue.front();
    		msgRecvQueue.pop();
    		//Because you will return if you enter here, you must unlock();
    		myMutex.unlock();
    		return true;
    	}
    	myMutex.unlock();
        //Other operation codes
    	return false;
    }
    

More advanced writing

②lock_guard class template

  • lock_ Guard < mutex > myguard (mymutex) directly replaces mymutex Lock() and mymutex unlock();
  • lock_ The guard constructor executes mutex::lock(); Execute mutex::unlock() when calling the destructor out of the scope
  • You can add {} to constrain lock_ Scope of guard;
//Function to fetch data
bool outMsgPro(int& command) {
    
    {
		std::lock_guard<std::mutex> myGuard(myMutex);
		if (!msgRecvQueue.empty()) {//Operate if it is not empty
			int command = msgRecvQueue.front();
			msgRecvQueue.pop();
			return true;
		}
    }
     //Other operation codes
	return false;
}

3, Deadlock

3.1 deadlock demonstration

Conditions for deadlock generation: there are at least two mutexes, which are required by multiple threads at the same time, and finally form a closed loop. For example:

  • A. When thread A executes, the thread locks mutex1 first, and the lock succeeds. Then, when it locks mutex2, context switching occurs.
  • b. Thread B executes. This thread locks mutex2 first because mutex2 is not locked, that is, mutex2 can be locked successfully, and then thread B wants to lock mutex1
  • c. At this time, a deadlock occurs. A locks mutex1 and needs to lock mutex2. B locks mutex2 and needs to lock mutex1 to form a closed loop and cannot continue to operate.

3.2 deadlock solution

As long as the locking order of multiple mutexes is the same, it will not cause deadlock!

3.3 std::lock() function template

  • std::lock(mutex1,mutex2...): lock multiple mutexes at a time (this is rarely the case in general), which is used to handle multiple mutexes. But the lock needs to be unlocked separately mutex1 unlock(),mutex2. Unlock () should be written by yourself
  • This function: if none of the mutexes is locked, it will wait until all the mutexes are locked. If one of them is not locked, the locked ones will be released (that is, either the mutexes are locked or not locked, and deadlock will be automatically prevented)

3.4 std::lock_guard and std::adopt_lock parameter

  • std::lock(mutex1,mutex2);
    lock_guard<mutex> myGuard1(mutex1, adopt_lock);
    lock_guard<mutex> myGuard2(mutex2, adopt_lock);
    

    Use lock_guard constructs mutex1 and mutex2 lock objects and adds adopt_ After lock, call lock_ When using the constructor of guard, lock() is no longer required; However, after the scope of the object is out, it will still call unlock() to release the lock! It solves the problem that each lock needs to be released after locking multiple locks.

  • std::adopt_lock is a structural object and serves as a marker to indicate that the mutex has locked (), and no lock() operation is required.

Entire sample code:

class A
{
public:
	//Function to fetch data
	bool outMsgPro(int& command) {

		lock(mutex1, mutex2);
		lock_guard<mutex> myGuard1(mutex1, adopt_lock);
		lock_guard<mutex> myGuard2(mutex2, adopt_lock);
        //②lock_guard<mutex> myGuard1(mutex1); lock_guard<mutex> myGuard2(mutex2);
		//①mutex1.lock(); mutex2.lock();
		if (!msgRecvQueue.empty()) {//Operate if it is not empty
			command = msgRecvQueue.front();
			msgRecvQueue.pop();
			//Because you will return if you enter here, you must unlock();
			//mutex1.unlock(); mutex2.unlock();
			
			return true;
		}
		//mutex1.unlock(); mutex2.unlock();
		return false; //return automatically releases the lock
	}
	//Write data function;
	void inMsgPro() {

		for (int i = 0; i < 100; ++i) {
			cout << "inMsgPro()Execute, insert element" << i << endl;

			lock(mutex1, mutex2);
			lock_guard<mutex> myGuard1(mutex1, adopt_lock);
			lock_guard<mutex> myGuard2(mutex2, adopt_lock);
            //②lock_guard<mutex> myGuard1(mutex1); lock_guard<mutex> myGuard2(mutex2);
			//①mutex1.lock(); mutex2.lock();
			msgRecvQueue.push(i);
			//mutex1.unlock(); mutex2.unlock();
		}
	}
	//Test data function
	void Test() {
		int data;
		for (int i = 0; i < 100; ++i) {
			if (outMsgPro(data)) cout << data << " ";
			else cout << "no data" << " ";
		}
	}
private:
	queue<int> msgRecvQueue;
	mutex mutex1;
	mutex mutex2;
};


int main() {
	A a;
	thread myInMsgObj(&A::inMsgPro, &a);//The address must be passed in, indicating that it is the same element
	thread myOutMsgObj(&A::Test, &a);
	myInMsgObj.join();
	myOutMsgObj.join();	
	return 0;
}

Tags: C++ Multithreading

Posted by rd321 on Thu, 12 May 2022 09:14:46 +0300