std::shared_mutex::lock(3)

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

πŸ’… std::shared_mutex::lock β€” C++ Standard Library realness

okay bestie, so this page is literally about std::shared_mutex::lock from the C++ Standard Library, and yes the name section of the original was just the name repeated twice, which is so unserious of it, but we move. πŸŽ€

void lock();  (since C++17)

🌸 Synopsis

So what does it do? It acquires an exclusive ownership of the shared_mutex. That's the whole vibe. ✨

But here's the important part, and I need you to actually read this one: if another thread is holding an exclusive lock or a shared lock on the same shared_mutex, then a call to lock will block execution until all such locks are released. Like, it just sits there waiting, no cap. And while shared_mutex is locked in exclusive mode, no other lock of any kind can also be held. None. Zero. The exclusivity is real. πŸ‘‘

🚨 okay this next bit is the dangerous one, pls pay attention: if lock is called by a thread that already owns the shared_mutex in any mode (exclusive or shared), the behavior is undefined. Not "throws", not "returns an error" β€” undefined. main character risk fr fr. πŸ’€

And one more lil technical gem: a prior unlock() operation on the same mutex synchronizes-with (as defined in std::memory_order) this operation. iykyk. 🧚

🫢 Parameters

(none) β€” she takes no arguments, low maintenance icon.

⭐ Return value

(none) β€” gives you nothing back, but honestly the exclusivity was the gift the whole time.

😭 Exceptions

Throws std::system_error when errors occur β€” including errors from the underlying operating system that would prevent lock from meeting its specifications. Rude of the OS honestly, but okay.

Good news tho: the mutex is not locked in the case of any exception being thrown. So if it throws, nothing got locked. Clean. No half-finished situationship with the mutex. πŸ™ˆ

πŸ”₯ Notes

okay so real talk: lock() is usually not called directly. Like, you can, but the cool kids use std::unique_lock, std::scoped_lock, and std::lock_guard to manage exclusive locking. That's the move. That's the whole point of those wrappers. πŸŽ€

🌈 Example

okay this one's fun, run it and see:

// Run this code

#include <chrono>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <syncstream>
#include <thread>
#include <vector>

std::mutex stream_mutx;
void print(auto const& v)
{
    std::unique_lock<std::mutex> lock(stream_mutx);
    std::cout << std::this_thread::get_id() << " saw: ";
    for (auto e : v)
        std::cout << e << ' ';
    std::cout << '\n';
}

int main()
{
    using namespace std::chrono_literals;
    constexpr int N_READERS = 5;
    constexpr int LAST = -999;

    std::shared_mutex smtx;
    int product = 0;

    auto writer = [&smtx, &product](int start, int end)
    {
        for (int i = start; i < end; ++i)
        {
            auto data = i;
            {
                std::unique_lock<std::shared_mutex> lock(smtx); //
better than:
                                                                //
smtx.lock();
                product = data;
            }
            std::this_thread::sleep_for(3ms);
        }

        smtx.lock(); // lock manually
        product = LAST;
        smtx.unlock();
    };

    auto reader = [&smtx, &product]
    {
        int data = 0;
        std::vector<int> seen;
        do
        {
            {
                // better to use:
                std::shared_lock lock(smtx); // smtx.lock_shared();
                data = product;
            }                                // smtx.unlock_shared();

            seen.push_back(data);
            std::this_thread::sleep_for(2ms);
        }
        while (data != LAST);

        print(seen);
    };

    std::vector<std::thread> threads;
    threads.emplace_back(writer, 1, 13);
    threads.emplace_back(writer, 42, 52);

    for (int i = 0; i < N_READERS; ++i)
        threads.emplace_back(reader);

    for (auto&& t : threads)
        t.join();
}

✨ Possible output:

127755840 saw: 43 3 3 4 46 5 6 7 7 8 9 51 10 11 11 12 -999
144541248 saw: 2 44 3 4 46 5 6 7 7 8 9 51 10 11 11 12 -999
110970432 saw: 42 2 3 45 4 5 47 6 7 8 8 9 10 11 11 12 -999
119363136 saw: 42 2 3 4 46 5 6 7 7 8 9 9 10 11 11 12 12 -999
136148544 saw: 2 44 3 4 46 5 6 48 7 8 9 51 10 11 11 12 12 -999

(notice how the reader threads each see a different lil slice of history β€” that's the shared/exclusive dance working πŸ’ƒ)

πŸ‘‘ See also

  • try_lock β€” tries to lock the mutex, returns if the mutex is not available (public member function)
  • unlock β€” unlocks the mutex (public member function)
  • lock_shared β€” locks the mutex for shared ownership, blocks if the mutex is not available (public member function)

and that's the page bestie!! sourced straight from http://cppreference.com, dated 2024.06.10, and it's literally titled std::shared_mutex::lock(3) πŸ’œ go forth and lock exclusively, but like, through std::unique_lock please. mwah. πŸ“

πŸ“¦ where this page comes from