C + + general lock management

Posted by bqheath on Wed, 02 Mar 2022 16:16:04 +0100

lock_guard

Class lock_guard is a mutex wrapper that provides a convenient RAII style mechanism for occupying mutexes during scope blocks.
Create lock_ When the guard object, it attempts to receive ownership of the given mutex. Control leave create lock_ Destroy lock when the scope of the guard object_ Guard and release mutex.
lock_ The guard class cannot be copied.

Member type
	mutex_type	Mutex Member function
 Member function
	(Constructor)	structure lock_guard ,Optionally lock the given mutex(Public member function)
	(Destructor)	Deconstruction lock_guard Object to unlock the underlying mutex(Public member function)
	operator=[Deleted]	Non reproducible assignment(Public member function)
#include <thread>
#include <mutex>
#include <iostream>
 
int g_i = 0;
std::mutex g_i_mutex;  // Protection g_i
 
void safe_increment()
{
    std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
 
    // g_i_mutex is automatically released when the lock leaves the scope
}
 
int main()
{
    std::cout << "main: " << g_i << '\n';
 
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
 
    t1.join();
    t2.join();
 
    std::cout << "main: " << g_i << '\n';
}
Possible outputs:

main: 0
140641306900224: 1
140641298507520: 2
main: 2

scoped_lock

Class scoped_lock is a mutex wrapper that provides a convenient RAII style mechanism. It occupies one or more mutexes during the existence of scope blocks.
Create scoped_lock object, which attempts to take ownership of the given mutex. Control the creation of scoped_ Destruct scoped when the scope of lock object_ Lock and release mutex. If several mutexes are given, the deadlock free algorithm is used, as with std::lock.
scoped_ The lock class is not replicable.

Member type
	mutex_type (if sizeof...(MutexTypes)==1)	Mutex , MutexTypes... Individual types in
 Member function
	(Constructor)	structure scoped_lock ,Optionally lock the given mutex(Public member function)
	(Destructor) 	Deconstruction scoped_lock Object to unlock the underlying mutex(Public member function)
	operator=[Deleted]	Not replicable(Public member function)
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>

struct Employee
{
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;
    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for (const auto &partner : lunch_partners)
            ret += partner + " ";
        return ret;
    }
};

void send_mail(Employee &, Employee &)
{
    // Simulate time-consuming sending operation
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }

    {
        // Using std::scoped_lock obtains two locks without worrying
        // Other assignment_ lunch_ Partner call deadlock
        // And it also provides a convenient RAII style mechanism

        std::scoped_lock lock(e1.m, e2.m);

        // Equivalent code 1 (using std::lock and std::lock_guard)
        // std::lock(e1.m, e2.m);
        // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

        // Equivalent code 2 (if unique_lock is required, for example, for condition variables)
        // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
        // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        // std::lock(lk1, lk2);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }

    send_mail(e1, e2);
    send_mail(e2, e1);
}

int main()
{
    Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");

    // Assign in parallel thread, because it takes a long time to send email on lunch assignment
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));

    for (auto &thread : threads)
        thread.join();
    std::cout << alice.output() << '\n'
              << bob.output() << '\n'
              << christina.output() << '\n'
              << dave.output() << '\n';
}

Possible outputs:

alice and bob are waiting for locks
alice and bob got locks
christina and bob are waiting for locks
christina and alice are waiting for locks
dave and bob are waiting for locks
dave and bob got locks
christina and alice got locks
christina and bob got locks
Employee alice has lunch partners: bob christina 
Employee bob has lunch partners: alice dave christina 
Employee christina has lunch partners: alice bob 
Employee dave has lunch partners: bob

unique_lock

Class unique_lock is a universal mutex wrapper that allows delayed locking, limited attempts to lock, recursive locking, ownership transfer, and use with conditional variables.

Class unique_lock is movable, but not replicable - it satisfies moveconstructible and moveassignable, but not copyconstructable or copyassignable.

Class unique_lock meets the basic lockable requirements. Unique if Mutex meets the lockable requirement_ Lock also meets the lockable requirements (for example, it can be used for std::lock); If Mutex meets the timedlockable requirement, then unique_lock also meets the requirements of timedlockable.

Member type
	mutex_type	Mutex
 Member function
	(Constructor)	structure unique_lock ,Optionally lock the mutex provided(Public member function)
	(Destructor)	If the association is mutually exclusive, the possession is unlocked(Public member function)
	operator=	If it is possessed, it unlocks the mutual exclusion and obtains the ownership of the other party(Public member function)
locking
	lock	Lock Association mutex(Public member function)
	try_lock	Try to lock the associated mutex, and return if mutex is unavailable(Public member function)
	try_lock_for	An attempt was made to lock the associated timed lock (TimedLockable) Mutex. If mutex is not available for a given duration, return(Public member function)
	try_lock_until	Attempting to lock an association can be timed to lock (TimedLockable) Mutex, if mutex is still unavailable at the specified time point, return(Public member function)
	unlock	Unlock Association mutex(Public member function)
Modifier
	swap	With another std::unique_lock Exchange status(Public member function)
	release	Disassociate the mutex without unlocking it(Public member function)
Observer
	mutex	Returns a pointer to the associated mutex(Public member function)
	owns_lock	Test whether the lock holds its associated mutex(Public member function)
	operator bool	Test whether the lock holds its associated mutex(Public member function)Nonmember function
	std::swap(std::unique_lock) (C++11)	std::swap yes unique_lock Specialization of(Function template)
#include <mutex>
#include <thread>
#include <chrono>
 
struct Box {
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int num)
{
    // The lock has not been actually removed
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
 
    // Lock two unique_lock without dead lock
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // 'from.m 'and' to M 'mutual exclusion unlocked in' unique '_ Lock 'destructor
}
 
int main()
{
    Box acc1(100);
    Box acc2(50);
 
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
 
    t1.join();
    t2.join();
}

shared_lock

Class shared_lock is a universal shared and mutually exclusive ownership wrapper, which allows delayed locking, timed locking and the transfer of lock ownership. Lock shared_lock, which locks the associated shared mutex in shared mode (std::unique_lock can be used to lock in exclusive mode).

shared_lock class is movable, but not replicable -- it meets the requirements of moveconstructible and moveassignable, but not copyconstructable or copyassignable.

shared_lock meets lockable requirements. If Mutex meets the sharedtimedlockable requirements, shared_lock also meets the requirements of timedlockable.

To wait for shared mutex in shared ownership mode, use std::condition_variable_any (std::condition_variable requires std::unique_lock, so it can only wait in the unique ownership mode).

Member type
	mutex_type	Mutex
 Member function
	(Constructor)	structure shared_lock ,Optionally lock the mutex provided(Public member function)
	(Destructor)	Unlock associated mutex(Public member function)
	operator=	If you own it, you can unlock the mutual exclusion and then get the ownership of the other party(Public member function)
Share lock
	lock	Lock associated mutex(Public member function)
	try_lock	Attempt to lock the associated mutex(Public member function)
	try_lock_for	Attempt to lock the associated mutex to specify the duration(Public member function)
	try_lock_until	Attempts to lock the associated mutex until the specified point in time(Public member function)
	unlock	Unlock mutually exclusive Association(Public member function)
Modifier
	swap	With another shared_lock Exchange data members(Public member function)
	release	Disassociate mutex Without unlocking(Public member function)
Observer
	mutex	Returns a pointer to the associated mutex(Public member function)
	owns_lock	Test whether the lock occupies its associated mutex(Public member function)
	operator bool	Test whether the lock occupies its associated mutex(Public member function)
Nonmember function
	std::swap(std::shared_lock)(C++14)	std::swap yes shared_lock Specialization of(Function template)

lock

Lock a given lockable object lock1, lock2,..., lockn, and use deadlock free algorithm to avoid deadlock.

To lock and try_ Unspecified series of calls to lock and unlock lock the object. If calling lock or unlock causes an exception, unlock is called on any locked object before re throwing.

#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
 
struct Employee {
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;
    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for( const auto& partner : lunch_partners )
            ret += partner + " ";
        return ret;
    }
};
 
void send_mail(Employee &, Employee &)
{
    // Simulate time-consuming sending operation
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }
 
    // Use std::lock to obtain two locks without worrying about assigning_ lunch_ Other calls from partners will deadlock us
    {
        std::lock(e1.m, e2.m);
        std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code (if unique_locks is required, for example, for condition variables)
//        std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
//        std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
//        std::lock(lk1, lk2);
// Better solution available in C++17
//        std::scoped_lock lk(e1.m, e2.m);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }
    send_mail(e1, e2);
    send_mail(e2, e1);
}
 
int main()
{
    Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
 
    // In parallel thread assignment, it takes a long time to send an email to the user to inform the lunch assignment
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
 
    for (auto &thread : threads) thread.join();
    std::cout << alice.output() << '\n'  << bob.output() << '\n'
              << christina.output() << '\n' << dave.output() << '\n';
}

Possible outputs:

alice and bob are waiting for locks
alice and bob got locks
christina and bob are waiting for locks
christina and bob got locks
christina and alice are waiting for locks
christina and alice got locks
dave and bob are waiting for locks
dave and bob got locks
Employee alice has lunch partners: bob christina 
Employee bob has lunch partners: alice christina dave 
Employee christina has lunch partners: bob alice 
Employee dave has lunch partners: bob

std::defer_lock, std::try_to_lock, std::adopt_lock

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

    • Join adopt_ After lock, call lock_ When using the constructor of guard, lock() is no longer required;
    • adopt_guard is a structure object and serves as a marker to indicate that the mutex has been locked () and does not need to be locked ().
  • unique_ The second parameter of lock std::adopt_lock:

    • Indicates that the mutex has been locked (), that is, it is not necessary to lock the mutex in the constructor.
    • Premise: lock in advance
  • std::try_to_lock:

    • Try to lock the mutex with mutex's lock(), but if the lock is not successful, it will return immediately and will not block there;
    • Use try_ to_ The reason for lock is to prevent other threads from locking mutex for too long, which causes this thread to block in lock all the time
    • Premise: cannot lock() in advance;
    • owns_ The locks () method determines whether to get the lock. If so, it returns true
  • std::defer_lock:

    • If there is no second parameter, lock mutex and add defer_lock starts a mutex without a lock
    • The purpose of not locking it is to call unique later_ Some methods of lock
    • Premise: cannot lock in advance

call_once

Function template. The first parameter of the function is a tag, and the second parameter is a function name.
Function: it can ensure that the function is called only once. It has the ability of mutex, and consumes less resources and is more efficient than mutex.
call_once() needs to be used in conjunction with a tag called std::once_flag; Actually once_flag is a structure called call_once() determines whether the function is executed through the tag. After the call is successful, the tag is set to a called state.

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag1, flag2;

void simple_do_once()
{
    std::call_once(flag1, []()
                   { std::cout << "Simple example: called once\n"; });
}

void may_throw_function(bool do_throw)
{
    if (do_throw)
    {
        std::cout << "throw: call_once will retry\n";
        // throw std::exception();
    }
    std::cout << "Didn't throw, call_once will not attempt again\n";
}

void do_once(bool do_throw)
{
    try
    {
        std::call_once(flag2, may_throw_function, do_throw);
    }
    catch (...)
    {
    }
}

int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);

    st1.join();
    st2.join();
    st3.join();

    std::thread t1(do_once, true);
    std::thread t2(do_once, false);
    std::thread t3(do_once, true);
    std::thread t4(do_once, true);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

It may be:

Simple example: called once
throw: call_once will retry
Didn't throw, call_once will not attempt again

Topics: C++ Multithreading