|
Flow 3.0.0
Flow project: Public API.
|
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... | |
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:
thread_locally_launch_rocket(). There's no way to signal them to do it immediately necessarily; but we can do it opportunistically to any thread that has already called this_thread_state() (been activated).To handle these challenges the pattern is as follows.
set<T*>: the set of Ts (each belonging to an activated thread that has called Thread_local_state_registry<T>::this_thread_state()) that should execute, and have not yet executed, thread_locally_launch_rocket().Thread_local_state_registry<T> is declared/instantiated – e.g., statically – also declare Polled_shared_state<set<T*>>. Conventionally we recommend the Polled_stared_state-first, Thread_local_state_registry-second order; the latter depends on the former.T a Poll_flag data member (say, Poll_flag m_missile_launch_needed_flag).So that's the setup/arming; and now to consume it:
this_thread_state() has been called in it (and therefore a T exists), whenever the opportunity arises, check the poll-flag, and in the rare case where it is armed, do thread_locally_launch_rocket():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.)
Each poll-flag, being a T member, lives exactly as long as its T. Hence:
registry.while_locked() (as shown above) is safe – even if a targeted thread is concurrently exiting: every T in state_per_thread, and therefore its flag, is alive at least until task() returns. (A T's thread-exit cleanup contends for the same lock and un-registers the T before destroying it.)T is available – including from ~T() itself, if it comes to that.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).
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.
atomic-logic that, while not exotic or obscure, is still easy to get wrong in little ways (that will not easily reveal themselves empirically) in terms of correctness and perf.| Shared_state_t | A 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. |
| 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.
| Ctor_args | See above. |
| shared_state_ctor_args | See above. |
| 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).
| Task | Function object matching signature void F(Shared_state*). |
| task | This will be invoked as follows: task(shared_state). shared_state shall point to the object stored in *this and constructed in our ctor. |