mutex and lock in C + + multithreading

Keywords: less

@

catalog

1, Basic concepts

In a multithreaded environment, when multiple threads compete for the same common resource, it is easy to cause thread safety problems. Therefore, we need to introduce a lock mechanism to ensure that only one thread can access public resources at any time.

  • Mutex is a class object, which can be understood as a lock. Multiple threads try to use the lock() member function to lock. Only one thread can lock successfully. If no lock succeeds, the process will continue to try to lock in lock().
  • You should be careful when using the mutex. There are not many or many data to protect. If you use less data, you will not get the effect, and your efficiency will be affected.

2, How to use

Include < mutex >

2.1 mutex.lock(),unlock()

Step: 1.lock(), 2. Operate shared data, 3.unlock().
lock() and unlock() should be used in pairs and cannot be locked and unlocked repeatedly. The essence is that the program (data) between lock and unlock will not be called and modified at the same time.

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        my_mutex.lock();
        cout<<"Insert data: "<<num<<endl;
        test_list.push_back(num);
        my_mutex.unlock();
    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        my_mutex.lock();
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"Retrieve data:"<<tmp<<endl;

        }
        my_mutex.unlock();

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

2.2 std::lock_guard class template

lock_ The guard constructor executes mutex::lock(). At the end of the scope, it automatically calls the destructor to execute mutex::unlock()

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        std::lock_guard<std::mutex> my_guard(my_mutex);
        cout<<"Insert data: "<<num<<endl;
        test_list.push_back(num);

    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        std::lock_guard<std::mutex> my_guard(my_mutex);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"Retrieve data:"<<tmp<<endl;

        }
     

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

2.2.1 std::lock_ STD:: adopt of guard_ Lock parameter

std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);

Add in adopt_ After lock, call lock_ When the constructor of guard is used, lock() is no longer performed;
adopt_guard is the structure object and serves as a mark to indicate that the mutex has been locked (), and it is not required to be locked ().

2.3 std::unique_lock function template

unique_lock wants to be better than lock_guard, based on the idea of RAII, also supports std::lock_guard's function, but the difference is that it provides more member functions, such as: lock(),unlock(), which is more flexible, and can be used with reference_ Variable is used together to control thread synchronization. But the efficiency is a little bit poor, and the memory is a little more occupied.

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

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<1000000000000000000;num++){
        std::unique_lock<std::mutex> my_guard(my_mutex);
        cout<<"Insert data: "<<num<<endl;
        test_list.push_back(num);

    }

}

void out_list(){

    for(int num=0;num<1000000000000000000; ++num){
        std::unique_lock<std::mutex> my_guard(my_mutex);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"Retrieve data:"<<tmp<<endl;

        }
     

    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}


2.3.1 unique_ The second parameter of lock

1) std::adopt_lock:

-Indicates that the mutex has been locked (), i.e. there is no need to lock the mutex in the constructor.
-Premise: lock in advance
 - lock_ This parameter can also be used in guard

2) std::try_to_lock:

  • Try to lock the mutex with mutx's lock(). If it is not locked successfully, it will return immediately and will not block there, but it can't operate the protected data (to prevent exceptions), only the unprotected data;
  • Use try_ To_ The reason for lock is to prevent other threads from locking mutex for too long, which causes the thread to block at the lock
  • Premise: cannot lock() in advance;
  • unique_ lock.owns_ The locks () method determines whether the lock is obtained. If it is obtained, it returns true
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

list<int> test_list;

mutex my_mutex;
void in_list(){
    for(int num=0;num<10000;num++){
        std::unique_lock<std::mutex> my_unique(my_mutex, std::try_to_lock);
        if(my_unique.owns_lock()){
            cout<<"Insert data: "<<num<<endl;
            test_list.push_back(num);
        }
        else{
            cout<<"I didn't get the lock. I had to do something else"<<endl;
        }


    }

}

void out_list(){

    for(int num=0;num<10000; ++num){


        std::unique_lock<std::mutex> my_unique(my_mutex);
        std::chrono::seconds dura(1);
        std::this_thread::sleep_for(dura);
        if(!test_list.empty()){
            int tmp = test_list.front();
            test_list.pop_front();
            cout<<"Retrieve data:"<<tmp<<endl;

        }
        else {
            cout<<"It's empty"<<endl;
        }


    }
}
int main()
{


    thread in_thread(in_list);
    thread out_thread(out_list);
    in_thread.join();
    out_thread.join();
    cout << "Hello World!" << endl;

    return 0;
}

3) std::defer_lock:

  • Plus defer_lock is a mutex without lock
  • The purpose of not locking it is to call the unique mentioned later_ Some methods of lock
  • Premise: can't lock in advance

2.3.2 unique_ Member function of lock

							(the first three with std::defer_lock)

1) lock(): lock

unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();

The function is: do not unlock() yourself;
2) unlock(): unlock.

unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//Handle some shared code
myUniLock.unlock();
//Handle some unshared code
myUniLock.lock();
//Handle some shared code

Function: because some unshared code needs to be processed, you can temporarily unlock() first, use other threads to process them, and then lock() after processing.
3)try_lock(): attempt to lock mutex
If you can't get the lock, return false, otherwise return true. Usage and previous try_ To_ The lock parameter is consistent.
4) release(): release unique_ mutex object pointer managed by 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
  • Usage mutex * PTX= myUniLock.release ():
    The ownership is taken over by PTX. If the mutex object handles the lock state, it needs to be unlocked by itself. ptx->unlock();

2.3.3 transfer of ownership

  1. Use move to transfer
    unique_lock myUniLock(myMutex); bind myMutex and myUniLock together, that is, myUniLock owns the ownership of myMutex
// myUniLock owns the ownership of myMutex. myUniLock can transfer its ownership of myMutex, but it cannot copy it.
unique_lock myUniLock2(std::move(myUniLock));
//Now myUniLock2 owns myMutex.
  1. return a temporary variable in the function, that is, transfer can be realized
unique_lock<mutex> aFunction()
{
    unique_lock<mutex> myUniLock(myMutex);
    //Moving constructors to return a local unique from a function_ Lock object is OK
    //Returning such local objects will cause the system to generate temporary unique_lock object and call unique_ Move constructor for lock
    return myUniLock;
}

2.4 std::lock() function template

  • std::lock(mutex1,mutex2…… ): locks multiple mutexes at a time, which is rarely used to handle multiple mutexes.
    If one of them is unlocked, the locked one will be released, and then it will wait until all the mutexes can be locked at the same time before continuing to execute. (either the mutexes are locked, or they are not locked to prevent deadlock)

3, Deadlock

3.1 causes

Deadlock has at least two mutex mutex mutex1 and mutex2.

  1. When thread A executes, the thread first locks mutex 1, and the lock succeeds. Then when mutex 2 is unlocked, context switching occurs.
  2. Thread B executes. This thread first locks mutex 2, because mutex 2 is not locked, that is, mutex 2 can be successfully locked, and then thread B will unlock mutex 1
  3. At this time, the deadlock occurs. A locks mutex 1, mutex 2, B locks mutex 2, mutex 1, and the two threads cannot continue to run...

3.2 solutions

As long as the lock sequence of multiple mutexes is the same, deadlock will not be caused.

4, Lock efficiency

The fewer the code segments of lock, the faster the execution, and the higher the efficiency of the whole program.

  1. Less code is locked, which is called fine granularity and high execution efficiency;
  2. There are many locked codes, which are called coarse granularity and low execution efficiency;

Posted by jeffkee on Mon, 01 Jun 2020 04:00:45 -0700