Defined in header <memoized_invoke.hh>

class lock_free;

lock_free is the synchronising ExecutionPolicy for memoized_invoke. It holds the run state in a std::atomic<execution_state> and makes the first-run race safe: exactly one thread runs the callable, and every other thread blocks until the result is published.

CTAD never selects it — construct with make_memoized<lock_free>(...).

How it works

StepMechanism
Claim the runtry_enter() is one compare_exchange_strong from not_started to running (memory_order_acq_rel / acquire). The single thread that succeeds runs the callable.
Everyone else waitswait() loads the state and, while it is running, parks on std::atomic<T>::wait (C++20) — futex-backed, no spin.
Publish successmark_done() stores done with memory_order_release, then notify_all(). A thread that observes done through the matching acquire sees the cached value.
Publish rollbackmark_free() stores not_started with memory_order_release, then notify_all() — used when the callable throws or on reset(). Woken waiters re-enter and retry.

Member functions

Same interface as single_threaded, every operation atomic:

FunctionEffect
bool try_enter()compare_exchange_strong not_startedrunning. true for the one winner.
void mark_done() noexceptStore done (release), notify_all().
void mark_free() noexceptStore not_started (release), notify_all().
bool is_done() const noexceptAcquire load == done.
void wait() const noexceptBlock while the state is running; return when it becomes done or not_started.

Special members — copy and move reset the state

lock_free() = default;                              // not_started
lock_free( lock_free const& ) noexcept;             // -> not_started
lock_free( lock_free&& ) noexcept;                  // -> not_started
lock_free& operator=( lock_free const& ) noexcept;  // -> not_started
lock_free& operator=( lock_free&& ) noexcept;       // -> not_started

std::atomic is neither copyable nor movable, and a copy owns a separate synchronisation context — a "done" flag copied from another object would not be backed by that object's release store. So every copy and move re-initialises the state to not_started.

At the memoized_invoke level this means: a copied or moved memoized_invoke<lock_free, …> keeps the copied cached value but reports is_done() == false and re-runs the callable on its first call. See memoizedinvoke::memoizedinvoke.

What lock_free does not synchronise

Only the first-run race. reset() and the argument-changing operator()(args...) mutate the stored argument tuple and the cached-value optional, which are ordinary non-atomic members of memoized_invoke. Calling either concurrently with anything else on the same object is a data race. Quiesce all other threads before resetting a shared lock_free wrapper.

Example

#include <memoized_invoke.hh>
#include <atomic>
#include <thread>
#include <vector>
#include <cassert>

using fedem::utility::make_memoized;
using fedem::utility::lock_free;

int main()
{
    std::atomic<int> runs{0};
    auto once = make_memoized<lock_free>(
        [&runs]{ runs.fetch_add(1); return 42; } );

    std::vector<std::thread> ts;
    std::vector<int> seen(16, 0);
    for (int i = 0; i < 16; ++i)
        ts.emplace_back([&, i]{ seen[i] = once(); });
    for (auto& t : ts) t.join();

    assert( runs.load() == 1 );
    for (int v : seen) assert( v == 42 );
}

See also