Mutex and lock of C + + standard library_ guard,recursive_mutex,timed_mutex,recursive_timed_mutex,unique_lock

1, mutex

  • The full name of mutex is mutual exclusion, which is an object used to help control "concurrent access to resources" in an exclusive and exclusive manner
  • For example, let's lock a resource
void f(int val);
 
int val;             //shared resource
std::mutex valMutex; //mutex 
 
void func()
{
    //Lock and then operate the shared resource
    valMutex.lock();
    if (val >= 0)
        f(val);
    else
        f(-val);
    //Unlock after access
    valMutex.unlock();
}

2, lock_guard

  • lock_guard is a class encapsulated by RAII, with the same function as mutex
  • It automatically locks the mutex during construction and unlock s the mutex in the destructor during deconstruction
  • Advantages over mutex:
    • Using mutex, we need to lock and unlock ourselves. If the mutex is locked, but the mutex is not unlocked after the resource is accessed, other methods of accessing the shared resource will be blocked forever
    • lock_ The advantage of guard is to automatically lock mutex during construction and unlock mutex when the scope ends / destructs
  • For example:
void f(int val);
 
int val;
std::mutex valMutex; //mutex 
 
int main()
{
    //Declare a lock with mutex_ Guard, which automatically locks the incoming mutex during construction
    std::lock_guard<std::mutex> lg(valMutex);
    if (val >= 0)
        f(val);
    else
        f(-val);
}//After the scope ends, lg destructs. In the destructor, it automatically unlock s mutex

3, mutex and lock_ The first demonstration case of guard

#include <iostream>
#include <thread>
#include <future>
#include <string>
using namespace std;
 
std::mutex printMutex;
 
void print(const std::string& s)
{
    //Lock the mutex to ensure that no other thread is printing when each thread is printing
    std::lock_guard<std::mutex> l(printMutex);
    for (char c : s) 
    {
        std::cout.put(c);
    }
    std::cout << std::endl;
}//mutex is automatically released when the scope ends
 
int main()
{
    auto f1 = std::async(std::launch::async, print, "Hello from a first thread");
    auto f2 = std::async(std::launch::async, print, "Hello from a second thread");
 
    print("Hello from the main thread");
}//Because the launch strategy of async() is std::launch::async, even if we don't use future Get() gets their results,
//But main() must wait for two threads to finish executing
  • The display results are as follows:

  • If mutex is not used in print, the displayed result may be:

4, recursive_mutex

  • Sometimes, recursive locking is necessary. A typical example is active object or monitor. They put a mutex in each public function and get its lock to place the data race and corrode the internal state of the object

Ordinary locks cannot perform recursive locking (deadlock) normally

  • For example, the following is a database class and its interfaces. mutex members are locked in each interface
class DatabaseAccess 
{
public:
    void createTable()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
    }
    void insertData()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
    }
    //In this interface, createTable() is called indirectly
    void createTableAndInsertData()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
        createTable();
    }
private:
    std::mutex dbMutex;
};
  • However, the above createTableAndInsertData() interface will cause deadlock because it locks mutex internally and then calls the createTable() interface. The createTable() interface will also lock mutex. However, because mutex has been locked, createTable() is deadlocked
  • If the platform detects deadlock similar to the above, the C + + standard library allows the second lock to throw an exception std::system_error with error code resource_deadlock_would_occur. But not necessarily, and often not

recursive_mutex

  • With recursive_mutex, there will be no problem with the above behavior. recursive_mutex allows the same thread to lock multiple times and release the lock at the last corresponding unlock
  • For example, we modify the DatabaseAccess class and its interface above:
class DatabaseAccess 
{
public:
    void createTable()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
    }
    void insertData()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
    }
    void createTableAndInsertData()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
        createTable();
    }
private:
    std::recursive_mutex dbMutex;
};

5, mutex's member function: tentative lock (try_lock())

  • try_ The lock () member function is used to lock mutex. If it can be locked, it returns true. If it cannot be locked, it does not block and returns false directly
std::mutex m;
 
//Try to lock m, and the while will not end until the locking is successful
while (m.try_lock() == false)
{
    doSomeOtherStuff();
}
 
//...
 
//Unlock after use
m.unlock();
  • If lock_guard wants to use try_ The lock added by lock () needs to pass an additional argument adopt_lock gives its constructor. For example:
std::mutex m;
 
//Try to lock m, and the while will not end until the locking is successful
while (m.try_lock() == false)
{
    doSomeOtherStuff();
}
//After locking, give mutex to lock_ Guard < > for management, you need to pass in std::adopt_lock parameter
std::lock_guard<std::mutex> lg(m, std::adopt_lock);
  • Note: try_lock() may fail falsely, that is, it may fail and return false even if it is not used by others (this behavior is provided for memory ordering, but it is not widely known)

6, lock with timeliness (timed_mutex, recursive_timed_mutex)

  • In order to wait for a specific time before locking, the so-called timed mutex can be used
  • STD:: timed is provided in the standard library_ Mutex and std::recursive_timed_mutex. And both classes provide try_lock_for() and try_lock_until() is used to wait for a certain period of time, or lock it until it reaches a certain point in time
  • For example:
std::timed_mutex m;
 
//Try locking for 1 second. If locking succeeds in 1 second, execute if
if (m.try_lock_for(std::chrono::seconds(1))) 
{
    //Set timed_mutex to lock_guard is managed. Note that its constructor needs to pass in std::adopt_lock
    std::lock_guard<std::timed_mutex> lg(m, std::adopt_lock);
}
else 
{
    couldNotGetTheLock();
}

7, Handle multiple locks (Global std::lock() function)

 

  • Sometimes, multiple mutex es need to be locked at a time (for example, to transfer data from one protected resource to another, two resources need to be locked at the same time)
  • If the previous lock mechanism is used, it may be complicated and risky: for example, you get the first lock but can't get the second lock, or deadlock (if you lock the same lock in different order)

Global std::lock() function

  • The global std::lock() function allows you to lock multiple mutex at once
  • For example:
std::mutex m1;
std::mutex m2;
 
void func()
{
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);
    //...
}//After the scope ends, m1 and m2 are automatically released
  • Precautions for std::lock():
    • This function locks all mutexes it receives and blocks until all mutexes are locked or until an exception is thrown
    • If an exception is thrown during locking, the mutex that has been locked will be released
    • After locking, you can cooperate with lock_guard uses mutex and needs to pass std::adopt_lock to lock_ Constructor for guard
    • This lock() function provides a deadlock avoidance mechanism, but the locking order of multiple locks is not clear

Global std::try_lock() function

 

  • This function can also lock multiple mutex, but it is actually used to try to lock. Its working principle is closely related to the return value. See the return value below for details
  • Return value:
    • If all mutex are locked successfully, return - 1. At this point, all mutex can be operated
    • If some locks are not locked successfully, the index of the first failed lock (starting from 0) is returned. At this time, the mutex that has been locked successfully will be released
  • For example:
std::mutex m1;
std::mutex m2;
 
void func()
{
    int idx = std::try_lock(m1, m2);
    //If both m1 and m2 are locked successfully, return - 1 and execute if
    if (idx < 0) 
    {
        std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
        std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);
    }
    else
    {
        std::cerr << "could not lock mutex m" << idx + 1 << std::endl;
    }
}
  • Note: this try_lock() does not provide a deadlock ratio mechanism, but it is guaranteed to appear in try_lock() attempts to lock mutex in the order of argument columns
  • Note: use the lock() function or try_ After the lock () function locks the mutex, you still need to unlock the mutex after the scope ends (unlock()), but generally we will pass it to lock_ Guard < > use

8, unique_lock

  • The standard library also provides a unique_ Lock < > class, which is more flexible against mutex
  • unique_lock < > interface and lock_ The interface of guard < > is the same, but it is allowed to write out "when to lock" and "how to lock or unlock" its mutex. In addition, unique_lock also provides owns_lock() and bool() interfaces to query whether its mutex is currently locked

Three locking signs

  • First: you can pass try_to_lock, which means to attempt to lock the mutex. If the locking is successful, it will return true. If the locking fails, it will not block but directly return false. The code is as follows:
std::mutex m;
 
void func()
{
    //Try locking m, but not blocking
    std::unique_lock<std::mutex> lock(m, std::try_to_lock);
    //If locking is successful, execute if
    if (lock)
    {
        //...
    }
}
//After the scope ends, if unique_lock successfully locks m, then unique_ The destructor of lock releases m; If not, its destructor does nothing
  • Second: you can pass a time period or point in time to the constructor, indicating that you try to lock mutex at a specific time
std::mutex m;
 
void func()
{
    //After 1 second, lock m. if the lock is successful, it will return. If the lock is failed, it will block and wait
    std::unique_lock<std::mutex> lock(m, std::chrono::seconds(1));
    //...
}
//After the scope ends, if unique_lock successfully locks m, then unique_ The destructor of lock releases m; If not, its destructor does nothing
  • Third: you can pass defer_lock gives its constructor to initialize unique_lock, but do not lock, but call the lock() member function to lock later
std::mutex m;
 
void func()
{
    //Just initialize lock with m, but do not lock
    std::unique_lock<std::mutex> lock(m, std::defer_lock);
    //...
    lock.lock(); //Call this member function to lock m
    //...
}
//After the scope ends, if unique_lock successfully locks m, then unique_ The destructor of lock releases m (no need to call unlock);
//If there is no destructor

std:::defer_lock flag

  • STD:: defer in the third usage mode above_ The lock flag can be used to create one or more locks and lock them later
  • For example:
std::mutex m1;
std::mutex m2;
std::mutex m3;
 
void func()
{
    std::unique_lock<std::mutex> lockM1(m1, std::defer_lock);
    std::unique_lock<std::mutex> lockM2(m2, std::defer_lock);
    std::unique_lock<std::mutex> lockM3(m3, std::defer_lock);
    //...
    std::lock(m1, m2, m3);
}
  • If there is no lock flag, then unique_lock and lock_ Like the guard, the mutex is directly locked
 

Demonstration case

  • With lock_guard and unique_lock as a tool, now we can implement an example to make one thread wait for another by polling a ready flag
bool readyFlag;
std::mutex readyFlagMutex;
 
void thread1()
{
    //Do some preparatory work for thread2
    //...
    std::lock_guard<std::mutex> lg(readyFlagMutex);
    readyFlag = true;
}
 
void thread2()
{
    //Wait for readyFlag to become true
    {
        std::unique_lock<std::mutex> ul(readyFlagMutex);
        //If the readyFlag is still not false, it means that thread1 has not been locked, so continue to wait
        while (!readyFlag)
        {
            ul.unlock();
            std::this_thread::yield();
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            ul.lock();
        }
    }//Release lock
 
    //After thread1 is locked, do the corresponding thing
}

9, Elaborate on mutex and lock

Elaborate on mutex

  • The standard library provides four mutex, as follows:

  • The comparison is as follows:

  • The operation functions of mutex are listed below:

  • The lock() member function may throw std::system_error with the following error code:
    • operation_not_permitted -- if the privilege level of the thread is insufficient to perform an operation
    • resource_ deadlock_ would_ Occurs - if the platform detects that a deadlock is about to occur
    • device_or_resource_busy -- if the mutex has been locked and cannot form blocking
  • If a program unlock s a mutex object that is not owned by it, or destroys a mutex object owned by any thread, or a thread owns a mutex object but ends its life, it will lead to ambiguous behavior
  • Note that when processing system time adjustment, try_lock_for() and try_lock_until() is usually different

Elaborate on lock_guard

  • The following figure lists lock_ Operation function of guard

Elaborate on unique_lock

  • unique_lock provides a lock guard for a mutex that does not have to be locked. The interface it provides is shown in the figure below:

  • Lock() may throw std::system_error, the error code it carries is the same as that caused by mutex's lock()
  • unlock() may throw std::system_error with error code operation_not_permitted - if the unique lock is not locked

Keywords: C++

Added by kr4mer on Sat, 19 Feb 2022 13:26:52 +0200