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