std::lock(3)

πŸ“¦ version 2024.07.05, in nixpkgs unstable, opensuse-leap 16.0

πŸ’– std::lock β€” C++ Standard Library πŸ’–

okay so bestie, welcome to std::lock(3) from the C++ Standard Library (the original header says C++ Standard Libary, typo and all β€” we love a chaotic icon πŸ’€). This is the deadlock-avoiding multi-lock function, and we're keeping every single detail. No cap.


🌸 NAME 🌸

std::lock β€” std::lock. That's it, that's the name, main character energy πŸ‘‘

✨ Synopsis ✨

Defined in header <mutex>.

template< class Lockable1, class Lockable2, class... LockableN >
void lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );

(since C++11) β€” yes bestie, she's modern.

Locks the given Lockable objects lock1, lock2, ..., lockn using a deadlock avoidance algorithm to avoid deadlock.

The objects are locked by an unspecified series of calls to lock, try_lock, and unlock. If a call to lock or unlock results in an exception, unlock is called for any locked objects before rethrowing. That's the safety net, and it's lowkey important 🫢

πŸŽ€ Parameters πŸŽ€

  • lock1, lock2, ... , lockn β€” the Lockable objects to lock. yes, all of them, bring the whole squad πŸ’…

πŸ’– Return value πŸ’–

(none) β€” literally nothing, she returns void energy ✨

πŸ¦‹ Notes πŸ¦‹

  • Boost provides a version of this function that takes a sequence of Lockable objects defined by a pair of iterators. cute crossover, no cap.
  • std::scoped_lock offers a RAII wrapper for this function, and is generally preferred to a naked call to std::lock. so if you're reaching for a naked std::lock, pause, bestie β€” usually you want the RAII wrapper πŸ”₯

🌈 Example 🌈

The following example uses std::lock to lock pairs of mutexes without deadlock. iconic behavior.

the original also has a // Run this code marker right here, so run it bestie πŸ‘‡

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

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 (auto n{lunch_partners.size()}; const auto& partner : lunch_partners)
            ret += partner + (--n ? ", " : "");
        return ret;
    }
};

void send_mail(Employee&, Employee&)
{
    // Simulate a time-consuming messaging operation
    std::this_thread::sleep_for(std::chrono::milliseconds(696));
}

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 acquire two locks without worrying about
    // other calls to assign_lunch_partner deadlocking 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 are needed, e.g. 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);
    // Superior 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");

    // Assign in parallel threads because mailing users about lunch assignments
    // takes a long time
    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 output πŸ“

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
Dave and Bob are waiting for locks
Dave and Bob got locks
Christina and Alice 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

πŸ‘‘ See also πŸ‘‘

  • unique_lock β€” implements movable mutex ownership wrapper. (C++11) (class template)
  • try_lock β€” attempts to obtain ownership of mutexes via repeated calls to try_lock. (C++11) (function template)
  • scoped_lock β€” deadlock-avoiding RAII wrapper for multiple mutexes. (C++17) (class template)

mwah πŸ’‹ that's the whole page. source/version/date vibes: http://cppreference.com, 2024.06.10, std::lock(3) β€” bye bestie, go lock responsibly 🫢✨

πŸ“¦ where this page comes from