randolf.ca  1.00
Randolf Richardson's C++ classes
Loading...
Searching...
No Matches
rthread
1#pragma once
2
3#include <pthread.h>
4
5#include <atomic>
6#include <exception>
7
8#include <randolf/rex>
9
10namespace randolf {
11
12 /*======================================================================*//**
13 @brief
14 This @ref rthread thread library provides an easier and sufficiently-complete
15 object-oriented thread interface class for C++ that adds a properly-honoured
16 destructor, and without terminating the entire application when the thread
17 throws an exception (a virtual method for optionally catching and handling
18 all exceptions is also provided).
19
20 POSIX threads are the foundation of the rthread class, and the resulting C++
21 interface will probably seem familiar to those who are familliar with Java.
22
23 @par Features
24 This is meant to be a safe and easy-to-use thread class for C++, which is
25 intended to make thread sub-classing straight-forward in a logical manner for
26 developers. Some of the key features are:
27
28 - Customizable constructor that doesn't automatically start the thread
29 - Better control with the @ref start() method to start a previously
30 constructed rthread
31 - Abstract @ref run() method (required)
32 - This method contains the code that will be run in its own thread
33 - Customizable destructor that occurs only after the thread ends (which
34 is particularly useful for daemon threads {a.k.a., detached threads})
35 - Customizable exception handler method (just subclass it to use it)
36 - Most methods are designed to be thread-safe (e.g., status methods)
37
38 Some advanced features are planned that exceed what the basic thread
39 functions provide, but are also needed:
40
41 - thread groups (separate class), possibly with support for including
42 threads in multiple groups
43 - thread pool (separate class)
44
45 @par Background
46 I created this class to make it easier to write internet server daemons. I
47 started out using C-style thread functions (because C++ doesn't come with a
48 thread class that can be sub-classed), and even with std::jthreads I ran into
49 a serious problem with the thread's destructor executing while the thread was
50 still running (the code that started it went out of scope) -- this is not
51 what I expected because the running thread should really be an additional
52 criteria for thread destruction (this is a serious concern since the
53 premature destruction of a running thread usually results in a segmentation
54 fault or other unpredictable behaviour that has the potential to cause the
55 Operating System to exhibit other strange behaviours or even crash entirely).
56
57 After looking for existing solutions (none of which resolved the ineherent
58 problems with threads in C++), I embarked on creating this rthread class,
59 which was highly educational but also wasn't difficult. The end result is a
60 small class that's easy to maintain, eliminates the reliability concerns, and
61 is easy to use primarily because C++ thoroughly supports subclassing.
62
63 My background in programming began when I was a young child, teaching myself
64 BASIC and then machine language (when I found BASIC to be too limited) before
65 moving on to other languages like Perl and Java many years later. Eventually
66 I circled around to C (which I chose to learn the hard way by writing some
67 PostgreSQL extensions) and then C++ a few years after that. I have a lot of
68 experience with programming threadded applications on a variety of platforms
69 that support pthreads (including Novell's NetWare), running application code
70 I created that ran hundreds of thousands of threads successfully serving a
71 similar number of users globally without slowdowns or crashes.
72 @par History
73 - 2022-Oct-22 v1.00 Initial version
74 - 2024-Oct-23 v1.00 Various minor improvements to the documentation since
75 the previous update
76 @version 1.00
77 @author Randolf Richardson
78
79 *///=========================================================================
80 class rthread {
81 private:
82 pthread_t __rthread_pthread_id = 0;
83 std::atomic_bool __rthread_running = false;
84 std::atomic_bool __rthread_detached = false;
85 std::atomic_bool __rthread_death = false; // Destructor changes this to TRUE
86 std::atomic_bool __rthread_reuseable = false; // Change this to a total-run-count // If set to TRUE, then this thread can be run again without needing to be re-constructed
87 std::atomic_ulong __rthread_used = 0; // Total number of times this thread was started
88
89 /*======================================================================*//**
90 Return Code check, and throws an rthread-specific exception if an error
91 occurred, which is indicated by @c -1 (a lot of example code shows @c < @c 1
92 comparisons, but we specifically test for @c -1 because the documentation
93 clearly states @c -1. This is important because a system that can support an
94 extremely high number of socket handles might, in theory, assign handles with
95 values that get interpreted as negative integers, and so less-than-zero tests
96 would result in dropped packets or dropped sockets (any such socket code that
97 allocates such handles obviously must not ever allocate @c -1 since this
98 would definitely be misinterpreted as an error).
99
100 If rc is not @c -1, then it is simply returned as is.
101
102 If n is nonzero and rc is 0, then n will override errno. (This option is
103 provided to accomodate the few socket library functions that return values
104 that are never errors, and expect the developer to rely on other means of
105 detecting whether an error occurred. This is an example of where Object
106 Oriented Programming is helpful in making things better.)
107 @returns Original value of @c rc (if no exceptions were thrown)
108 *///=========================================================================
109 const int __rc_check_pthread(
110 /// Return code
111 const int rc,
112 /// Exception handling flags (see @ref rex::REX_FLAGS for details)
113 const int flags = rex::rex::REX_FLAGS::REX_DEFAULT) {
114 if (rc != 0) {
115 randolf::rex::mk_exception("", rc, flags); // This function doesn't return (this code ends here)
116 } // -x- if rc -x-
117 return rc;
118 }; // -x- int __rc_check_pthread -x-
119
120 public:
121 /*======================================================================*//**
122 @brief
123 Default constructor.
124 You can subclass this constructor and/or create parameterized constructors,
125 any of which may throw exceptions (even though this default one doesn't).
126
127 This is particularly useful for performing pre-run setup before starting the
128 thread (see the @ref run() and @ref start() methods for details), especially
129 with threads that start running in response to external events (such as user
130 interaction events, incoming internet socket connections, etc.).
131 *///=========================================================================
132 rthread() noexcept {}; // -x- constructor rthread -x-
133
134 /*======================================================================*//**
135 @brief
136 Default destructor. Called only after two conditions are met:
137 1. this rthread has gone out of scope
138 2. termination of the underlying pthread (this is different from the
139 standard C++ implementation of threads and jthreads, which don't have
140 this criteria)
141
142 Exceptions are handled by the @ref _rthread_exceptions() method before this
143 destructor is activated.
144
145 You can add your own destructor to safely ensure that files and sockets are
146 closed, and that resources are freed, deallocated, deleted, etc., which is
147 essential to preventing various types of resource leaks if your thread code
148 (in the @ref run() method) exited early due to an unexpected exception (it is
149 the developer's responsibility to ensure resources are managed properly, and
150 this destructor provides a means of handling this reliably).
151
152 Logging, re-queuing, semaphore completions, etc., can also be handled in this
153 method because it runs after the termination of the underlying pthread.
154 @warning
155 Do not throw exceptions from this method. If you're relying on a function or
156 a class that might throw exceptions (intended or otherwise), then you need to
157 take care to at least utilize a final try/catch block for @c std::exception.
158 *///=========================================================================
159 virtual ~rthread() noexcept { // Destructor (subclass destructor will run first if it's defined)
160 __rthread_death = true;
161 if (__rthread_running && !__rthread_detached) {
162 pthread_detach(__rthread_pthread_id);
163 __rthread_detached = true;
164 } // -x- if __rthread_running -x-
165 if (__rthread_running) pthread_cancel(__rthread_pthread_id); // This must be the very last; any commands after this will cause: terminate called without an active exception
166 // TODO: pthread_setcancelstate, pthread_setcanceltype, and pthread_testcancel
167 }; // -x- destructor rthread -x-
168
169 /*======================================================================*//**
170 @brief
171 Final rthread exception handler.
172 If an uncaught exception is thrown from the @ref run() method in a running
173 rthread, then it will be handled by this method (which is still technically
174 running on the underlying pthread). Override this method to handle known and
175 unknown exceptions gracefully, before the destructor (or reinitiator) runs.
176 @warning
177 If @c e is @c nullptr it means an unknown exception was thrown (e.g.,
178 internally we literally caught `(...)` (other exception), in which case
179 you'll need to use @c std::current_exception() to access it.
180 @note
181 For a reuseable rthread, the @ref running status will be set to false during
182 @ref _rthread_reinitialize execution, which can be useful for handling those
183 exceptions separately within this code (by testing for the @ref running()
184 condition).
185 *///=========================================================================
186 virtual void _rthread_exceptions(
187 /// Exceptions (see warning, immediately above)
188 std::exception* e) noexcept {}; // -x- void rthread_exceptions -x-
189
190 /*======================================================================*//**
191 @brief
192 Re-initialize internal variables, refresh resources, etc., before re-running
193 a @ref reuseable rthread.
194 When utilizing the reuseable feature, this method can be thought of as having
195 similar functionality to that of the constructor, except that it is used to
196 do the following to bring internal variables and resources back to the same
197 state that the constructor originally initialized them to, and thus reducing
198 construction overhead (some variables may not need re-initializing, which can
199 also help to further-reduce overhead):
200 - close any open files, sockets, etc., that need to be closed (or reset
201 file pointers instead of re-opening files)
202 - reset/clear variables and memory structures to their expected defaults
203 (or free-and-allocate resources that can't be reset or cleared reliably)
204 - re-add @c this to a thread pool or concurrent queue of ready rthreads
205
206 @note
207 If there is any clean-up, logging, etc., peformed in the destructor that also
208 needs to be performed here, the best approach is to create a private method
209 for those particular actions and then call it from this method as well as the
210 destructor for better operational consistency.
211 @see reuseable
212 *///=========================================================================
213 virtual void _rthread_reinitialize() noexcept {}; // -x- void rthread_reinitialize -x-
214
215 /*======================================================================*//**
216 @brief
217 Daemonize this rthread.
218
219 If this rthread was previously detached, then this method has no effect (but
220 won't cause the randolf::rex::xEINVAL exception to be thrown; there may be
221 other reasons for this exception to be thrown).
222 @par Threads
223 This method is thread-safe.
224
225 @throws randolf::rex::xEINVAL Not a joinable rthread
226 @throws randolf::rex::xESRCH A pthread with the underlying @c pthread_id
227 couldn't be found (underlying pthread doesn't exist)
228
229 @returns The same rthread object so as to facilitate stacking
230 @see detached
231 @see start_detached
232 *///=========================================================================
233 rthread* detach() {
234 if (__rthread_running && !__rthread_detached) {
235 __rc_check_pthread(pthread_detach(__rthread_pthread_id));
236 __rthread_detached = true;
237 } // -x- if __rthread_running -x-
238 return this;
239 }; // -x- rthread* detach -x-
240
241 /*======================================================================*//**
242 @brief
243 Find out whether this rthread is daemonized.
244 @par Threads
245 This method is thread-safe.
246 @returns TRUE = daemonized
247 @returns FALSE = not daemonized
248 @see detach
249 @see start_detached
250 *///=========================================================================
251 bool detached() noexcept {
252 return __rthread_detached;
253 }; // -x- bool detached -x-
254
255 /*======================================================================*//**
256 @brief
257 Join this rthread to the current thread that called this method. (This is
258 how non-daemonized threads are normally terminated.)
259 @note
260 This method blocks until this rthread's underlying pthread is terminated.
261 @par Threads
262 This method is thread-safe.
263
264 @throws randolf::rex::xEDEADLK Deadlock detected because two threads are
265 queued to join with each other, or the current rthread is trying to
266 join with itself
267 @throws randolf::rex::xEINVAL Not a joinable rthread
268 @throws randolf::rex::xEINVAL A different thread is already queued to join
269 with this rthread
270 @throws randolf::rex::xESRCH A pthread with the underlying @c pthread_id
271 couldn't be found (underlying pthread doesn't exist)
272
273 @returns The same rthread object so as to facilitate stacking
274 @see stop
275 *///=========================================================================
276 rthread* join() {
277 if (__rthread_running) {
278 __rthread_running = false; // Do this before pthread_join to signal an immediate shutdown
279 __rc_check_pthread(pthread_join(__rthread_pthread_id, NULL));
280 __rthread_detached = true; // TODO: Determine if this should actually be set to FALSE
281 } // -x- if __rthread_running -x-
282 return this;
283 }; // -x- rthread* join -x-
284
285 /*======================================================================*//**
286 @brief
287 Find out what the underlying @c pthread_id is.
288
289 Advanced developers may want easy access to this ID for a variety of reasons.
290
291 @warning
292 This method is not thread-safe.
293
294 @returns ID number of underlying pthread (0 = not assigned, which is what you
295 should expect to find if this rthread didn't @ref start() yet)
296 *///=========================================================================
297 pthread_t pthread_id() noexcept {
298 return __rthread_pthread_id;
299 }; // -x- pthread_t pthread_id -x-
300
301 /*======================================================================*//**
302 @brief
303 Find out whether this rthread can be re-used.
304 @par Threads
305 This method is thread-safe.
306 @returns TRUE = re-useable
307 @returns FALSE = not re-useable
308 *///=========================================================================
309 bool reuseable() noexcept {
310 return __rthread_reuseable;
311 }; // -x- bool reuseable -x-
312
313 /*======================================================================*//**
314 @brief
315 Configure whether this rthread can be re-used.
316 @par Threads
317 This method is thread-safe.
318 @returns The same rthread object so as to facilitate stacking
319 @see _rthread_reinitialize
320 *///=========================================================================
321 rthread* reuseable(
322 /// TRUE = Set this rthread to be re-useable@n
323 /// FALSE = Set this rthread to not be re-useable
324 bool flag) noexcept {
325 __rthread_reuseable = flag;
326 return this;
327 }; // -x- rthread* reuseable -x-
328
329 /*======================================================================*//**
330 @brief
331 Thread functionality (subclassing this method is required).
332 @warning
333 Do not call this method directly. To properly begin rthread execution, use
334 either of the @ref start() or @ref start_detached() methods.
335 When subclassing rthread, you need to override this method with the code that
336 is to be executed as a separate thread. (You don't have to move all of your
337 code into this method; in fact, you can simply use this method to call out to
338 code elsewhere, or call methods on instantiated classes that were referenced
339 during the instantiation of your overriding rthread() constructor, etc.)
340 @note
341 Minimally, this is the only method that's required when sub-classing rthread.
342 @see detach
343 @see detached
344 @see start
345 @see start_detached
346 *///=========================================================================
347 virtual void run() = 0; // Not requiring this method in the subclass is completely and utterly pointless
348
349 /*======================================================================*//**
350 @brief
351 Find out how many times this rthread was started.
352 This method is only really meaningful for @ref reuseable rthreads.
353 @par Threads
354 This method is thread-safe.
355 @returns Run count
356 @see _rthread_reinitialize
357 @see reuseable
358 *///=========================================================================
359 ulong run_count() noexcept {
360 return __rthread_used;
361 }; // -x- ulong run_count -x-
362
363 /*======================================================================*//**
364 @brief
365 Find out whether this rthread is actively running.
366 @par Threads
367 This method is thread-safe.
368 @returns TRUE = running
369 @returns FALSE = not running
370 @see start
371 @see stop
372 *///=========================================================================
373 bool running() noexcept {
374 return __rthread_running;
375 }; // -x- bool running -x-
376
377 /*======================================================================*//**
378 @brief
379 Commence execution of the @c run() method as a separate rthread.
380 @par Threads
381 This method is thread-safe.
382
383 @throws randolf::rex::xEWOULDBLOCK (xEAGAIN) Insufficient resources to start
384 the underlying pthread
385 @throws randolf::rex::xEWOULDBLOCK (xEAGAIN) Maximum number-of-threads limit
386 reached
387
388 @returns The same rthread object so as to facilitate stacking
389 @see detach
390 @see detached
391 @see start_detached
392 @see stop
393 *///=========================================================================
394 rthread* start() {
395 __rc_check_pthread(pthread_create(&__rthread_pthread_id, NULL, __rthread_run, this));
396 __rthread_used++; // Increment thread run count
397 return this;
398 }; // -x- rsocket* start -x-
399
400 /*======================================================================*//**
401 @brief
402 Commence execution of the @c run() method as a separate thread in a
403 daemonized state.
404 @par Threads
405 This method is thread-safe.
406
407 @throws randolf::rex::xEDEADLK Deadlock detected because two threads are
408 queued to join with each other, or the current rthread is trying to
409 join with itself
410 @throws randolf::rex::xEINVAL Not a joinable rthread
411 @throws randolf::rex::xEINVAL A different thread is already queued to join
412 with this rthread
413 @throws randolf::rex::xENOMEM Insufficient memory (on Linux this normally
414 always succeeds, but there's not guarantee as the community suggests
415 that this could change in the future)
416 @throws randolf::rex::xESRCH A pthread with the underlying @c pthread_id
417 couldn't be found (underlying pthread doesn't exist)
418
419 @returns The same rthread object so as to facilitate stacking
420 @see detach
421 @see detached
422 @see start
423 @see stop
424 *///=========================================================================
425 rthread* start_detached() { // Use this instead of calling detach() separately to prevent the highly improbable instance of detaching a thread after it already finished
426 pthread_attr_t attr;
427 __rc_check_pthread(pthread_attr_init(&attr));
428 __rc_check_pthread(pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED));
429 __rc_check_pthread(pthread_create(&__rthread_pthread_id, &attr, __rthread_run, this));
430 __rthread_detached = true;
431 __rthread_used++; // Increment thread run count
432 return this;
433 }; // -x- rthread* start_detached -x-
434
435 /*======================================================================*//**
436 @brief
437 Request the graceful termination of this rthread.
438 @warning
439 One side-effect of this method is to daemonize this rthread because stop()
440 serves as an alternative to the @ref join() method.
441 @note
442 It is the responsibility of developers to include periodic checks throughout
443 their code (assumedly at convenient points, which is common) by utilizing the
444 @ref running() method to determine whether to start winding things down.
445 @par Threads
446 This method is thread-safe.
447
448 @throws randolf::rex::xEINVAL Not a joinable rthread
449 @throws randolf::rex::xESRCH A pthread with the underlying @c pthread_id
450 couldn't be found (underlying pthread doesn't exist)
451
452 @returns The same rthread object so as to facilitate stacking
453 @see join
454 @see start
455 @see start_detached
456 *///=========================================================================
457 rthread* stop() noexcept { // This doesn't cancel the thread; it just indicates that it needs to stop running -- it's up to the code in the subclassed run() method to check periodically if it should stop by using the running() method
458 if (__rthread_running) {
459 detach();
460 __rthread_running = false;
461 }// -x- if __rthread_running -x-
462 return this;
463 }; // -x- rthread* stop -x-
464
465 private:
466 /*======================================================================*//**
467 @brief
468 This internal method starts the underlying pthread, and does the following:
469
470 1. catches any exceptions that the thread might have thrown (and, if any
471 were caught, in turn calls the @ref exceptions() method)
472
473 2. if this rthread is reuseable, makes preparations for re-use, including
474 calling the re-constructor method (if it was subclassed)
475
476 3. if this rthreads is not reuseable, affect its destruction
477
478 @par Threads
479 This method is thread-safe.
480 *///=========================================================================
481 static void* __rthread_run(
482 // Pointer to subclassed rthread object, which @c pthread_create() passes as type @c void*
483 void* arg) noexcept {
484
485 // --------------------------------------------------------------------------
486 // Alias (void*)arg to (rthread*)r so we can just write r-> instead of having
487 // write ((rthread*)arg)-> repeatedly (the compiler will effectively do this
488 // for us).
489 // --------------------------------------------------------------------------
490 rthread* r = (rthread*)arg;
491
492 // --------------------------------------------------------------------------
493 // Indicate that this rthread is running.
494 //
495 // We must set this before starting the underlying thread to avoid a
496 // potential race condition if there's a loop within the thread that first
497 // checks whether the thread is in a running state.
498 // --------------------------------------------------------------------------
499 r->__rthread_running = true; // This will be set to false in the destructor (or the reinitiator)
500
501 // --------------------------------------------------------------------------
502 // Call the subclassed run() method.
503 // --------------------------------------------------------------------------
504 try {
505 r->run();
506 } catch (std::exception* e) { // Handle a known exception
507 r->_rthread_exceptions(e);
508 } catch (...) { // Handle an unknown exception
509 r->_rthread_exceptions(nullptr);
510 }
511
512 // --------------------------------------------------------------------------
513 // This is where it becomes possible to reuse an rthread. The amount of work
514 // required to just terminate a non-reusable (default) rthread is very little
515 // compared to preparing an rthread to be reuseable at this point, but the
516 // trade-off is worthwhile when performing a minimal (and probably partial)
517 // re-initialization whilst also avoiding re-instantiating an rthread object.
518 // --------------------------------------------------------------------------
519 if (r->__rthread_reuseable) { // This rthread is reuseable
520 r->__rthread_detached = false; // Reset internal flags because each thread will begin as not-detached
521 r->__rthread_running = false; // Clear the "running" status
522 try {
523 r->_rthread_reinitialize();
524 } catch (std::exception* e) { // Handle a known exception
525 r->_rthread_exceptions(e);
526 } catch (...) { // Handle an unknown exception
527 r->_rthread_exceptions(nullptr);
528 }
529 } else { // This rthread is not reuseable
530 delete r; // Initiate destructor(s) since rthread is not reuseable
531 } // -x- if __rthread_reuseable -x-
532
533 return r; // Same as "return arg;" but in this case we're expressing intent by returning "r"
534 }; // -x- void* __rthread_run -x-
535
536 }; // -x- class rthread -x-
537
538} // -x- namespace randolf -x-