Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
CallbackRegistry.h
Go to the documentation of this file.
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Execution_CallbackRegistry_h_
5#define _Stroika_Foundation_Execution_CallbackRegistry_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <atomic>
10#include <compare>
11#include <condition_variable>
12#include <functional>
13#include <memory>
14#include <mutex>
15#include <thread>
16#include <vector>
17
18#include "Stroika/Foundation/Common/Common.h"
21
22/**
23 * \file
24 *
25 * \note Code-Status: <a href="Code-Status.md#Beta">Beta</a>
26 *
27 * TODO:
28 * @todo https://github.com/SophistSolutions/Stroika/issues/1210 - a choice of what to do with a callback's exception
29 */
30
32
33 /**
34 * \brief The callbacks an object calls when something happens - each added, and removed by the ID Add gave it - with the
35 * guarantees that needs when they are called on a thread of the object's own, and added and removed on others.
36 *
37 * o Call holds no lock while a callback runs. So Add never waits for a running callback, and a callback may Add,
38 * Remove (itself too) or Call, or wait for a thread doing so.
39 * o Once Remove returns, that callback is not running, and is never called again - except that, called from within
40 * that very callback, the call already under way finishes. So Remove waits for a call running on another thread:
41 * do not call it holding a lock that callback takes.
42 * o An exception from a callback is logged (DbgTrace) and ignored, and the rest are still called - except
43 * Thread::AbortException, which ends the Call, so its thread can end.
44 * o 'Before' and 'after' are as the registry's lock orders them - each Add, Remove and RemoveAll, and the start of
45 * each Call, takes it in turn (from different threads at once, in no defined order). So a Call calls the callbacks
46 * added before it started, and not removed since: one added during a Call is first called by the next.
47 *
48 * Its lock is a plain (non-recursive) mutex, held only to read or update the list of callbacks - never while one runs,
49 * so nothing can re-enter it - and so cheap: one uncontended lock to Add or Remove; to Call, one, and two more for each
50 * callback it runs. (A use that cannot afford even that need not use this.)
51 *
52 * \note Defined only for callbacks returning void: Call runs any number of them, so has no one result to give back - and
53 * choosing one (the first's, the last's, or all of them, as boost::signals2's 'combiners' do) is a different
54 * abstraction.
55 *
56 * \note It replaces Execution::Function's job of making callbacks removable - a footgun. A Function equals only copies of
57 * itself (each construction is given a new number), so removing a callback works only with the Function added, or
58 * a copy of it - and nothing enforces that, as a lambda converts to a Function implicitly:
59 * \code
60 * monitor.AddCallback ([this] (const Event& e) { OnEvent_ (e); });
61 * ...
62 * monitor.RemoveCallback ([this] (const Event& e) { OnEvent_ (e); }); // a new Function: removes nothing
63 * \endcode
64 * That compiles, runs, and silently leaves the callback in place - to be called after its 'this' is gone. And the
65 * equality is that wrapper's own, around a copyable function<>, where the standard's wrappers - function<>, C++23's
66 * move_only_function, C++26's copyable_function - compare only to nullptr, by design. Here the registration, not
67 * the callable, has the identity: Add returns an ID, the only thing Remove takes, so there is nothing to get wrong;
68 * and nothing is asked of a callback but to be called (so, once C++23 is the floor, it could be a
69 * move_only_function).
70 *
71 * \note \em Thread-Safety <a href="Thread-Safety.md#Internally-Synchronized-Thread-Safety">Internally-Synchronized-Thread-Safety</a>
72 *
73 * \par Example Usage
74 * \code
75 * CallbackRegistry<void (const Event&)> fOnEvent; // in an object that calls them from a thread of its own
76 * ...
77 * auto id = fOnEvent.Add ([] (const Event& e) { DbgTrace ("{}"_f, e); });
78 * fOnEvent.Call (e); // on that thread: each callback, in the order added
79 * fOnEvent.Remove (id); // once this returns, that callback is not running, and is not called again
80 * \endcode
81 */
82 template <typename FUNCTION_SIGNATURE>
84
85 template <typename... ARGS>
86 class CallbackRegistry<void (ARGS...)> {
87 public:
88 using Callback = function<void (ARGS...)>;
89
90 public:
91 /**
92 * \brief Names a callback Add added, for Remove. No two the same in a process - so one from another CallbackRegistry
93 * removes nothing; nor does ID{}, which names none.
94 */
96
97 public:
98 CallbackRegistry () = default;
99 CallbackRegistry (const CallbackRegistry&) = delete;
100
101 public:
102 /**
103 * \pre no Call under way, on any thread - not just no callback running (@see RemoveAll)
104 */
106
107 public:
108 nonvirtual CallbackRegistry& operator= (const CallbackRegistry&) = delete;
109
110 public:
111 /**
112 * \brief Add callback, to be called by each Call from now on (not one already under way). Never waits for a running
113 * callback.
114 *
115 * \pre callback != nullptr
116 */
117 nonvirtual ID Add (const Callback& callback);
118
119 public:
120 /**
121 * \brief Once this returns, that callback is not running, and is never called again - except that, called from within
122 * it, the call already under way finishes. Removing one not added, or already removed, does nothing.
123 */
124 nonvirtual void Remove (ID id) noexcept;
125
126 public:
127 /**
128 * \brief Remove every callback: once this returns, none is running, and none is called again - except that, called from
129 * within one, the call already under way finishes (and the rest of that Call calls none).
130 *
131 * Why: so an owner can go away while another thread may be calling its callbacks. Once this returns, nothing a callback
132 * uses - the owner, what the callbacks captured - is in use, or will be, so it can be destroyed. And called from within
133 * a callback (an owner its own callback destroys), it returns, rather than waiting forever for that callback to end.
134 *
135 * It removes the callbacks added before it, in its lock's order (@see CallbackRegistry). One added after - even by
136 * another thread while this waits for a running callback - is not removed, and each Call from then on calls it, as any
137 * other. So an owner going away must first stop whatever might add one.
138 *
139 * \note The registry itself must still outlive any Call under way, which - though it calls no more callbacks - still
140 * takes the registry's lock: destroy it only once the thread calling Call has stopped, or keep it in a shared_ptr
141 * that thread holds too.
142 */
143 nonvirtual void RemoveAll () noexcept;
144
145 public:
146 /**
147 * \brief Call each callback, in the order added, with args - holding no lock while one runs. An exception from a
148 * callback is logged and ignored, except Thread::AbortException.
149 */
150 nonvirtual void Call (ARGS... args);
151
152 private:
153 struct Entry_ : Memory::UseBlockAllocationIfAppropriate<Entry_> {
154 Entry_ (ID id, const Callback& callback);
155 ID fID;
156 Callback fCallback;
157 bool fRemoved{false}; // guarded by fMutex_
158 vector<thread::id> fRunningOn; // guarded by fMutex_: the threads calling it now (the same one, twice, if Calls nest)
159 };
160
161 private:
162 mutable mutex fMutex_;
163 condition_variable fCallEnded_; // with fMutex_: for a Remove waiting for a running callback
164 vector<shared_ptr<Entry_>> fEntries_; // guarded by fMutex_
165 };
166
167}
168
169/*
170 ********************************************************************************
171 ***************************** Implementation Details ***************************
172 ********************************************************************************
173 */
174#include "CallbackRegistry.inl"
175
176#endif /*_Stroika_Foundation_Execution_CallbackRegistry_h_*/
An opaque ID - each New () one never given before, in this process - of a type of its own for each TA...
Definition UniqueID.h:50
The callbacks an object calls when something happens - each added, and removed by the ID Add gave it ...