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