Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
ThreadPool.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_ThreadPool_h_
5#define _Stroika_Foundation_Execution_ThreadPool_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <functional>
10#include <list>
11#include <mutex>
12
14#include "Stroika/Foundation/Containers/Collection.h"
16#include "Stroika/Foundation/Execution/WaitableEvent.h"
17
18/**
19 * \file
20 *
21 * \note Code-Status: <a href="Code-Status.md#Beta">Beta</a>
22 *
23 * TODO:
24 * @todo AddTask () implementation is HORRIBLE (for case where fAddBlockTimeout != 0) and needs to be rewritten with condition variables.
25 *
26 * @todo ThreadPool::WaitForTask () is a very sloppy inefficient implementation (but probably rarely called).
27 *
28 * @todo CONSIDER USE OF blocking q - I think it will help. Or figure out
29 * how these tie together. Or rewrite using condition_variable.
30 *
31 * @todo Current approach to aborting a running task is to abort the thread. But the current
32 * thread code doesn't support restarting a thread once its been aborted. We PROBABLY
33 * should correct that at some point - and allow a thread to undo its abort-in-progress.
34 * However - no need immediately. Instead - the current ThreadPool implementation simply
35 * drops that thread and builds a new one. Performance overhead yes - but only for the
36 * likely rare case of aborting a running task.
37 *
38 * @todo Consider adding the idea of TaskGroups - which are properties shared by all tasks (or some tasks)
39 * added to a threadpool. If multiple tasks are added to the threadpool with the same
40 * TaskGroup, then they respect that task-group's constraints. One example constraint would be
41 * mutual run exclusion. This would allow you to create lockless threaded procedures, because
42 * they would be guaranteed to not be run all at the same time, and yet could STILL leverage
43 * the benefits of thread pooling.
44 *
45 * For example, if you had 3 threads in the pool, and 5 thread groups, then typically one or
46 * more thread groups would be idle. The thread groups give you lockless execution, and
47 * the threadpool lets the 5 groups run 'at the same time' on only 3 threads.
48 */
49
51
52 /**
53 * The ThreadPool class creates a small fixed number of Thread objects, and lets you use them
54 * as if there were many more. You submit a task (a std::function) - named by the TaskID AddTask returns - and it gets
55 * eventually executed.
56 *
57 * If as Task in the thread pool raises an exception - this will be IGNORED (except for the
58 * special case of Thread::AbortException which is used internally to end the threadpool or
59 * remove some threads). Because of this, your submitted runnable should take care of its own
60 * error handling internally.
61 *
62 * ThreadPool mainly useful for servicing lost-cost calls/threads, where the overhead
63 * of constructing the thread is significant compared to the cost of performing the action,
64 * and where the priority & stacksize can all be predetermined and 'shared'.
65 * Also - where you want to CONTROL the level of thread creation (possibly to avoid
66 * DOS attacks or just accidental overloading).
67 *
68 * \pre Debug::AppearsDuringMainLifetime (); during the lifetime of the ThreadPool
69 *
70 * \note \em Thread-Safety <a href="Thread-Safety.md#Internally-Synchronized-Thread-Safety">Internally-Synchronized-Thread-Safety</a>
71 * all methods can be freely used from any thread, and they will block internally as needed.
72 *
73 * \note pointless to ever create threadpool and not use, so [[nodiscard]] appropriate
74 */
75 class [[nodiscard]] ThreadPool {
76 public:
77 /**
78 * Affects behavior of AddTask command. If a max provided, then attempts to Add
79 * will check this versus the actual pending Q length, and wait/throw if insufficient space.
80 *
81 * Use this overload to assure thread pools don't 'blow up' with too many tasks.
82 *
83 * \note - often only provide QMax::fLength, since the default to not blocking - just throwing cuz Q full - is quite reasonable.
84 */
85 struct QMax {
86 size_t fLength{0};
87
88 /**
89 * Zero timeout means never wait (AddTask either adds or throws but doesn't wait for Q to empty enuf).
90 */
91 Time::DurationSeconds fAddBlockTimeout{0s};
92 };
93
94 /**
95 */
96 struct Options {
97 /**
98 * number of threads allocated to service tasks.
99 */
100 unsigned int fThreadCount{thread::hardware_concurrency ()};
101
102 /**
103 * Name used in various debug messages.
104 */
105 optional<Characters::String> fThreadPoolName;
106
107 /**
108 * Default to no max Q - infinite queue size. Note, a value of fLength=fThreadCount, or some small mutiple, would often be reasonable.
109 */
110 optional<QMax> fQMax;
111
112 /**
113 * defaults false;
114 */
115 bool fCollectStatistics{false};
116 };
117
118 public:
119 /**
120 * \par Example Usage
121 * \code
122 * ThreadPool p{ThreadPool::Options{.fThreadCount = 3}};
123 * p.AddTask ([&q, &counter] () {
124 * ..dostuff..
125 * });
126 * // when goes out of scope automatically blocks waiting for threads to complete...
127 * // or call p.WaitForTasksDoneUntil ()
128 * \endcode
129 */
130 ThreadPool ();
131 ThreadPool (const Options& options);
132 ThreadPool (ThreadPool&&) = delete;
133 ThreadPool (const ThreadPool&) = delete;
134
135 public:
136 nonvirtual ThreadPool& operator= (ThreadPool&&) = delete;
137 nonvirtual ThreadPool& operator= (const ThreadPool&) = delete;
138
139 public:
140 /**
141 * Destroying a threadpool implicitly calls AbortAndWaitForDone () and eats any errors (cannot rethrow).
142 *
143 * \note - ThreadPool used to have explicit abort methods, but there was no point. When aborting, you must wait for them all to
144 * shut down to destroy the object. And since you cannot restart the object, there is no point in ever aborting without destroying.
145 * so KISS - just destroy the ThreadPool object.
146 */
147 ~ThreadPool ();
148
149 public:
150 /**
151 * \note It is important (required) that all tasks added to a ThreadPool respond in a timely manner to Thread Abort.
152 * ThreadPool counts on that for clean shutdown.
153 *
154 * This means periodically calling CheckForInterruption () and that any waits respect thread cancelation (stop_token).
155 *
156 * Tasks may exit via exception, but nothing will be done with that exception (beyond DbgTrace logging). So generally
157 * not a good idea, except for ThreadAbort handling.
158 */
159 using TaskType = function<void ()>;
160
161 public:
162 /**
163 * \brief Names a task AddTask added - for AbortTask, IsPresent, IsRunning and WaitForTask. No two the same in a process, so
164 * one from another ThreadPool names none of this one's; nor does TaskID{}.
165 *
166 * \note Why an ID, and not the task, to name one by: @see CallbackRegistry (and the same task can be added twice, too).
167 */
169
170 public:
171 /**
172 * These options have have been modified by various APIs, and reflect the current state of options, not necessarily those that the
173 * ThreadPool was created with.
174 */
175 nonvirtual Options GetOptions () const;
176
177 public:
178 /**
179 * This returns the number of threads in the pool (not the number of tasks). Note 0 is a legal size.
180 */
181 nonvirtual unsigned int GetPoolSize () const;
182
183 public:
184 /**
185 * SetPoolSize () is advisory. It attempts to add or remove entries as requested. Note - 0 is a legal size.
186 *
187 * But under some circumstances, it will fail. For example, if tasks are busy
188 * running on all threads, the number of threads in the pool cannot be decreased.
189 *
190 * @todo - WE CAN do better than this - at least marking the thread as to be removed when the
191 * task finishes - but NYI
192 */
193 nonvirtual void SetPoolSize (unsigned int poolSize);
194
195 public:
196 /**
197 * Push the given task into the queue (possibly blocking til space in the Q or throwing if no space in Q, but does NOT block waiting for Q to till).
198 *
199 * \par Example Usage
200 * \code
201 * ThreadPool p;
202 * p.AddTask ([] () {doIt ();});
203 * \endcode
204 *
205 * if qmax is provided, it takes precedence over any default value associated with the ThreadPool (constructor). If neither
206 * provided (as an argument or associated with the pool, this is treated as no max, and the addition just proceeds.
207 *
208 * If qMax provided (even indirectly), assure task q length doesn't exceed argument by waiting up to the qMax
209 * duration, and either timing out, or successfully add the task.
210 *
211 * Returns the TaskID naming the task added - for AbortTask, IsPresent, IsRunning and WaitForTask.
212 */
213 nonvirtual TaskID AddTask (const TaskType& task, const optional<Characters::String>& name = nullopt);
214 nonvirtual TaskID AddTask (const TaskType& task, QMax qmax, const optional<Characters::String>& name = nullopt); ///< \brief ... waiting for (or throwing if no) room in the queue, per qmax
215
216 private:
217 nonvirtual TaskID AddTask_ (const TaskType& task, const optional<Characters::String>& name);
218
219 public:
220 /**
221 * It is NOT an error to call this with a task that is not in the Queue
222 * (since it would be a race to try to find out if it was already executed.
223 *
224 * It can cancel a task if it has not yet been started, or EVEN if its already in
225 * progress (see Thread::Abort - it sends abort signal)
226 *
227 * The function doesn't return until the task has been successfully cancelled, or it throws if timeout.
228 */
229 nonvirtual void AbortTask (TaskID task, Time::DurationSeconds timeout = Time::kInfinity);
230
231 public:
232 /**
233 * See AbortTask () - it aborts all tasks - if any.
234 */
235 nonvirtual void AbortTasks (Time::DurationSeconds timeout = Time::kInfinity);
236
237 public:
238 /**
239 * returns true if queued OR actively running.
240 */
241 nonvirtual bool IsPresent (TaskID task) const;
242
243 public:
244 /**
245 * returns true actively running
246 */
247 nonvirtual bool IsRunning (TaskID task) const;
248
249 public:
250 /**
251 * throws if timeout. Returns when task has completed (or if not in task q)
252 */
253 nonvirtual void WaitForTask (TaskID task, Time::DurationSeconds timeout = Time::kInfinity) const;
254
255 public:
256 /**
257 */
258 struct TaskInfo {
259 TaskID fID;
260 TaskType fTask;
261 optional<Characters::String> fName;
262 optional<Time::TimePointSeconds> fRunningSince; // if missing, cuz not running
263
264 nonvirtual bool IsRunning () const;
265 };
266
267 public:
268 /**
269 * \brief returns GetPendingTasks () + GetRunningTasks () - but also some extra information about each task
270 */
271 nonvirtual Containers::Collection<TaskInfo> GetTasks () const;
272
273 public:
274 /**
275 * return all tasks which are queued, but haven't yet been assigned to a thread.
276 */
277 nonvirtual Containers::Collection<TaskID> GetPendingTasks () const;
278
279 public:
280 /**
281 * return all tasks which are currently running (assigned to some thread in the thread pool).
282 * \note - this is a snapshot in time of something which is often rapidly changing, so by the time
283 * you look at it, it may have changed (but since we use shared_ptrs, its always safe to look at).
284 */
285 nonvirtual Containers::Collection<TaskID> GetRunningTasks () const;
286
287 public:
288 /**
289 * \brief return total number of tasks, either pending, or currently running.
290 *
291 * This is GetRunningTasks().size () + GetPendingTasks ().size (), or alternatively GetTasks.size (), but more efficient.
292 */
293 nonvirtual size_t GetTasksCount () const;
294
295 public:
296 /**
297 * \brief return GetPendingTasks ().size (), except probably much more efficient
298 */
299 nonvirtual size_t GetPendingTasksCount () const;
300
301 public:
302 /**
303 * Wait for the given amount of time for all (either given argument or all tasks in this thread pool) to be done.
304 *
305 * When called with a specific set of tasks, this procedure waits for exactly those tasks. When called with no task
306 * argument, it waits until GetTaskCount () == 0.
307 *
308 * \note For the all-tasks overload, if new tasks are added to the thread pool, those are waited for too.
309 * But - its perfectly legal to add new tasks after this returns, so the task count could increase (if tasks were added) after
310 * this returns without exception (obviously there are likely more tasks if it returns with a timeout exception).
311 *
312 * \note ***Cancelation Point***
313 */
314 nonvirtual void WaitForTasksDone (const Traversal::Iterable<TaskID>& tasks, Time::DurationSeconds timeout = Time::kInfinity) const;
315 nonvirtual void WaitForTasksDone (Time::DurationSeconds timeout = Time::kInfinity) const;
316
317 public:
318 /**
319 * Wait for the given amount of time for all (either given argument or all tasks in this thread pool) to be done.
320 *
321 * When called with a specific set of tasks, this procedure waits for exactly those tasks. When called with no task
322 * argument, it waits until GetTaskCount () == 0.
323 *
324 * \note For the all-tasks overload, if new tasks are added to the thread pool, those are waited for too.
325 * But - its perfectly legal to add new tasks after this returns, so the task count could increase (if tasks were added) after
326 * this returns without exception (obviously there are likely more tasks if it returns with a timeout exception).
327 *
328 * \note ***Cancelation Point***
329 */
330 nonvirtual void WaitForTasksDoneUntil (const Traversal::Iterable<TaskID>& tasks, Time::TimePointSeconds timeoutAt) const;
331 nonvirtual void WaitForTasksDoneUntil (Time::TimePointSeconds timeoutAt) const;
332
333 public:
334 /**
335 * NOTE - see https://github.com/SophistSolutions/Stroika/issues/1127 (STK-995) for how we might cheaply significantly improve these stats
336 */
337 struct Statistics {
338 unsigned int fNumberOfTasksAdded{0};
339
340 /**
341 * Reports number of tasks that ran to completion, possibly with exceptions, possibly without
342 */
343 unsigned int fNumberOfTasksCompleted{0};
344
345 /**
346 * This is the divisor for fTotalTimeConsumed, and may not include all tasks for a variety of reasons (canceled etc).
347 * It doesn't contain tasks not yet run. Number of tasks reporting COULD exceed number added (since you can reset counters
348 * while there are tasks in queue).
349 */
350 unsigned int fNumberOfTasksReporting{0};
351
352 Time::DurationSeconds fTotalTimeConsumed{0.0};
353
354 /**
355 *
356 */
357 optional<Time::DurationSeconds> GetMeanTimeConsumed () const;
358
359 /**
360 * See Characters::ToString ()
361 */
362 nonvirtual Characters::String ToString () const;
363 };
364
365 public:
366 /**
367 * \pre (GetOptions ().fCollectStatistics);
368 */
369 nonvirtual void ResetStatistics ();
370
371 public:
372 /**
373 * \pre (GetOptions ().fCollectStatistics);
374 */
375 nonvirtual Statistics GetCurrentStatistics () const;
376
377 public:
378 /**
379 * a helpful debug dump of the ThreadPool status
380 */
381 nonvirtual Characters::String ToString () const;
382
383 public:
384 [[deprecated ("Since Stroika v3.0d5 use Options")]] ThreadPool (unsigned int tc, const optional<Characters::String>& name = nullopt)
385 : ThreadPool{Options{tc, name}}
386 {
387 }
388
389 private:
390 bool fCollectStatistics_{false};
391 Statistics fCollectedTaskStats_;
392
393 private:
394 nonvirtual void Abort_ () noexcept;
395
396 private:
397 nonvirtual void AbortAndWaitForDone_ () noexcept;
398
399 private:
400 class MyRunnable_;
401
402 private:
403 struct TPInfo_ {
404 Thread::Ptr fThread;
405 shared_ptr<MyRunnable_> fRunnable;
406 };
407
408 private:
409 struct PendingTaskInfo_ {
410 TaskID fID;
411 TaskType fTask;
412 optional<Characters::String> fName;
413 };
414
415 private:
416 // Called internally from threadpool tasks - to wait until there is a new task to run.
417 // This will not return UNTIL it has a new task to proceed with (except via exception like Thread::AbortException)
418 nonvirtual void WaitForNextTask_ (PendingTaskInfo_* result);
419 nonvirtual TPInfo_ mkThread_ ();
420
421 private:
422 mutable mutex fCriticalSection_; // fCriticalSection_ protects fThreads_ and fPendingTasks_ and the fields of the MyRunnable_ members inside each thread (fThreads_).
423 // Each should be a very short critical section, except for SetPoolSize()
424 atomic<bool> fAborted_{false};
425 optional<QMax> fDefaultQMax_;
426 Containers::Collection<TPInfo_> fThreads_; // all threads, and a data member for thread object, and one for running task, if any
427 list<PendingTaskInfo_> fPendingTasks_; // tasks not yet running - somewhat like a queue, but allow remove from middle
428 WaitableEvent fTasksMaybeAdded_{}; // recheck for new tasks (or other events - wakeup waiters on fTasks);
429 atomic<unsigned int> fNextThreadEntryNumber_{1};
430 optional<Characters::String> fThreadPoolName_;
431
432 private:
433 friend class MyRunnable_; // So MyRunnable_ can call WaitForNextTask_()
434 };
435
436}
437
438/*
439 ********************************************************************************
440 ***************************** Implementation Details ***************************
441 ********************************************************************************
442 */
443#include "ThreadPool.inl"
444
445#endif /*_Stroika_Foundation_Execution_ThreadPool_h_*/
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
A Collection<T> is a container to manage an un-ordered collection of items, without equality defined ...
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:234