Flow 3.0.0
Flow project: Public API.
Public Types | Public Member Functions | List of all members
flow::util::Polled_shared_state< Shared_state_t > Class Template Reference

Optional-use companion to Thread_local_state_registry that (together with Poll_flag) enables the Polled_shared_state pattern wherein from some arbitrary thread user causes the extant thread-locally-activated threads opportunistically collaborate on/using locked shared state, with the no-op fast-path being gated by a high-performance Poll_flag being disarmed. More...

#include <thread_lcl.hpp>

Inherits boost::noncopyable.

Public Types

using Shared_state = Shared_state_t
 Short-hand for template parameter type.
 

Public Member Functions

template<typename... Ctor_args>
 Polled_shared_state (Ctor_args &&... shared_state_ctor_args)
 Forwards to the stored object's Shared_state ctor. More...
 
template<typename Task >
void while_locked (const Task &task)
 Locks the non-recursive shared-state mutex, such that no access or modification of the contents of the Shared_state shall concurrently occur; executes given task; and unlocks said mutex. More...
 

Detailed Description

template<typename Shared_state_t>
class flow::util::Polled_shared_state< Shared_state_t >

Optional-use companion to Thread_local_state_registry that (together with Poll_flag) enables the Polled_shared_state pattern wherein from some arbitrary thread user causes the extant thread-locally-activated threads opportunistically collaborate on/using locked shared state, with the no-op fast-path being gated by a high-performance Poll_flag being disarmed.

This Polled_shared_state pattern (I, ygoldfel, made that up... don't know if it's a known thing) is maybe best explained by example. Suppose we're using Thread_local_state_registry with Thread_local_state type being T. Suppose that sometimes some event occurs, in an arbitrary thread (for simplicity let's say that is not in any thread activated by the Thread_local_state_registry<T>) that requires each state to execute thread_locally_launch_rocket(). Lastly, suppose that upon launching the last rocket required, we must report success via report_success() from whichever thread did it.

However there are 2-ish problems at least:

To handle these challenges the pattern is as follows.

registry.while_locked([&](const auto& state_per_thread) // Access across per-thread state is done while_locked().
{
if (state_per_thread.empty()) { return; } // No missiles to launch for sure; forget it.
// Load the shared state (while_locked()):
missiles_to_launch_polled_shared_state.while_locked([&](set<T*>* threads_that_shall_launch_missiles)
{
// *threads_that_shall_launch_missiles is protected against concurrent change.
for (const auto& state_and_mdt : state_per_thread)
{
T* const thread_lcl_state_ptr = state_and_mdt.first;
threads_that_shall_launch_missiles->insert(thread_lcl_state_ptr);
}
});
// *AFTER!!!* loading the shared state, arm the poll-flag:
for (const auto& state_and_mdt : state_per_thread)
{
T* const thread_lcl_state_ptr = state_and_mdt.first;
thread_lcl_state_ptr->m_missile_launch_needed_flag.arm_next_poll();
// (We arm every per-thread T; but it is possible and fine to do it only for some.)
// Also note it might already be armed; this would keep it armed; no problem. Before the for()
// the set<> might already have entries (launches planned, now we're adding possibly more to it).
}
});

So that's the setup/arming; and now to consume it:

void opportunistically_launch_when_triggered() // Assumes: registry.this_thread_state_or_null()) is non-null.
{
T* const this_thread_state = registry.this_thread_state();
if (!this_thread_state->m_missile_launch_needed_flag.poll_armed())
{ // Fast-path! Nothing to do re. missile-launching.
return;
}
// else: Slow-path. Examine the shared-state; do what's needed. Note: poll_armed() would now return false.
missiles_to_launch_polled_shared_state.while_locked([&](set<T*>* threads_that_shall_launch_missiles)
{
if (threads_that_shall_launch_missiles->erase(this_thread_state) == 0)
{
// Already-launched? Bug? It depends on your algorithm. But the least brittle thing to do is likely:
return; // Nothing to do (for us) after all.
}
// else: Okay: we need to launch, and we will, and we've marked our progress about it.
thread_locally_launch_rocket();
if (threads_that_shall_launch_missiles->empty())
{
report_success(); // We launched the last required missile... report success.
}
});
}

Hopefully that explains it. It is a little rigid and a little flexible; the nature of Shared_state is arbitrary, and the above is probably the simplest form of it (but typically we suspect it will usually involve some container(s) tracking some subset of extant T*s).

If there is no shared state to maintain – the armed flags alone carry all the needed info – then you need no Polled_shared_state at all: just embed the Poll_flags. (In the above example that would have been enough – if not for the requirement to report_success(), when the last missile is launched.)

Lifetime/safety

Each poll-flag, being a T member, lives exactly as long as its T. Hence:

Performance

The arming event occurs rarely and thus is not part of any fast-path; the fast-path cost is thread-local logic detecting poll_armed() == false first-thing and doing nothing further – a single, cheap atomic op (see Poll_flag doc header).

Design rationale: addendum

The two actual API-classes involved in the Polled_shared_state pattern – Polled_shared_state here and Poll_flag nearby – are each quite simple, so simple that one could say they are unnecessary. Nevertheless in our experience it was worthwhile to do this; in particular the sister project Flow-IPC as of this writing uses the pattern (including both classes) in 3 different places.

Template Parameters
Shared_state_tA single object of this type shall be constructed and can be accessed, whether for reading or writing, using while_locked(). It must be constructible via the ctor signature you choose to use when constructing *this Polled_shared_state ctor (template). The ctor args shall be forwarded to the Shared_state_t ctor.

Constructor & Destructor Documentation

◆ Polled_shared_state()

template<typename Shared_state_t >
template<typename... Ctor_args>
flow::util::Polled_shared_state< Shared_state_t >::Polled_shared_state ( Ctor_args &&...  shared_state_ctor_args)

Forwards to the stored object's Shared_state ctor.

Next: use while_locked() to check/modify Shared_state contents safely; then arm the relevant per-thread Poll_flags. See class doc header for the full pattern.

Template Parameters
Ctor_argsSee above.
Parameters
shared_state_ctor_argsSee above.

Member Function Documentation

◆ while_locked()

template<typename Shared_state_t >
template<typename Task >
void flow::util::Polled_shared_state< Shared_state_t >::while_locked ( const Task &  task)

Locks the non-recursive shared-state mutex, such that no access or modification of the contents of the Shared_state shall concurrently occur; executes given task; and unlocks said mutex.

Behavior is undefined (actually: deadlock) if task() calls this->while_locked() (the mutex is non-recursive).

Template Parameters
TaskFunction object matching signature void F(Shared_state*).
Parameters
taskThis will be invoked as follows: task(shared_state). shared_state shall point to the object stored in *this and constructed in our ctor.

The documentation for this class was generated from the following file: