thread_supervisor
supervisor.h
Go to the documentation of this file.
1 /**
2  @file
3  @author Alexander Sherikov
4  @copyright 2022 Alexander Sherikov. Licensed under the Apache License,
5  Version 2.0. (see LICENSE or http://www.apache.org/licenses/LICENSE-2.0)
6  @brief
7 */
8 
9 
10 #pragma once
11 
12 #include <thread>
13 #include <functional>
14 #include <atomic>
15 #include <mutex>
16 #include <iostream>
17 #include <chrono>
18 #include <list>
19 
20 #include <pthread.h>
21 
22 #include "util.h"
23 
24 
25 namespace tut
26 {
27  namespace log
28  {
29  /**
30  * Simple stderr logger, can be overridden by corresponding template
31  * parameter.
32  */
33  class StdErr
34  {
35  public:
36  template <class t_Arg, class... t_Args>
37  void log(const t_Arg &arg, t_Args &&...args) const
38  {
39  std::cerr << arg;
40  // cppcheck-suppress ignoredReturnValue
41  log(std::forward<t_Args>(args)...);
42  }
43 
44  void log() const
45  {
46  std::cerr << std::endl;
47  }
48  };
49  } // namespace log
50 
51 
52  namespace thread
53  {
54  /// Thread parameters
55  class Parameters
56  {
57  public:
58  /// What to do when thread terminated
59  enum class TerminationPolicy
60  {
61  IGNORE, /// ignore and proceed
62  KILLALL /// kill all other threads
63  };
64 
65  /// Exception handling policy
66  enum class ExceptionPolicy
67  {
68  CATCH, /// Catch & report
69  PASS /// application is going to crash
70  };
71 
72 
73  /// Restarting parameters
74  class Restart
75  {
76  public:
77  std::size_t attempts_; /// 0 = unlimited
78  std::size_t sleep_ms_; /// delay between attempts
79 
80  public:
81  explicit Restart(const std::size_t attempts = 0, const std::size_t sleep_ms = 0) // NOLINT
82  {
83  attempts_ = attempts;
84  sleep_ms_ = sleep_ms;
85  }
86 
87  [[nodiscard]] bool isUnlimited() const
88  {
89  return (0 == attempts_);
90  }
91 
92  [[nodiscard]] bool isOk(const std::size_t attempt) const
93  {
94  return (isUnlimited() or attempt < attempts_);
95  }
96 
97  [[nodiscard]] bool isEnabled() const
98  {
99  return (isOk(1));
100  }
101 
102  void wait() const
103  {
104  std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms_));
105  }
106  };
107 
108 
109  /// Scheduling parameters, POSIX threads only, see pthread_setschedparam
111  {
112  public:
113  int priority_; /// thread priority
114  int policy_; /// thread policy
115  bool ignore_failures_; /// proceed if scheduling parameters could not be set
116 
117  public:
118  explicit Scheduling(
119  const int priority = 0, // NOLINT
120  const int policy = SCHED_FIFO, // NOLINT
121  const bool ignore_failures = true)
122  {
123  priority_ = priority;
124  policy_ = policy;
125  ignore_failures_ = ignore_failures;
126  }
127 
128 
129  bool apply(const std::thread::native_handle_type &&handle) const // NOLINT return value can be ignored
130  {
131  if (SCHED_FIFO != policy_ or 0 != priority_) // custom parameters
132  {
133  sched_param sched_params;
134  sched_params.sched_priority = priority_;
135 
136  if (0 != pthread_setschedparam(handle, policy_, &sched_params))
137  {
138  return (false);
139  }
140  }
141  return (true);
142  }
143  };
144 
145 
146  public:
149 
150 #ifdef THREAD_SUPERVISOR_THOU_SHALT_NOT_PASS /// do not allow threads to exit / crash quietly
153 #else
156 #endif
157 
158 
159  public:
160  template <class... t_Args>
161  Parameters(const Restart &&restart, t_Args &&...args) // NOLINT noexplicit
162  : Parameters(std::forward<t_Args>(args)...)
163  {
164  restart_ = restart;
165  }
166 
167  template <class... t_Args>
168  Parameters(const Scheduling &&scheduling, t_Args &&...args) // NOLINT noexplicit
169  : Parameters(std::forward<t_Args>(args)...)
170  {
171  scheduling_ = scheduling;
172  }
173 
174  template <class... t_Args>
175  Parameters(const TerminationPolicy termination_policy, t_Args &&...args) // NOLINT noexplicit
176  : Parameters(std::forward<t_Args>(args)...)
177  {
178  termination_policy_ = termination_policy;
179  }
180 
181  template <class... t_Args>
182  Parameters(const ExceptionPolicy exception_policy, t_Args &&...args) // NOLINT noexplicit
183  : Parameters(std::forward<t_Args>(args)...)
184  {
185  exception_policy_ = exception_policy;
186  }
187 
188  Parameters() = default;
189  };
190 
191 
192 
193  template <class t_Logger = log::StdErr>
194  class Supervisor;
195 
196 
197  /// Thread handling class
198  class Thread
199  {
200  // THREAD_SUPERVISOR_DISABLE_CLASS_COPY(Thread)
201 
202  public:
203  using List = std::list<Thread>;
204  using Reference = List::iterator;
205 
206  public:
208  std::thread thread_;
210 
211 
212  protected:
213  template <class t_Logger, class t_Function, class... t_Args>
214  void startOnce(Supervisor<t_Logger> *supervisor, t_Function &&function, t_Args &&...args)
215  {
217  {
219  std::bind(function, std::forward<t_Args>(args)...)();
220  break;
221 
223  try
224  {
225  std::bind(function, std::forward<t_Args>(args)...)();
226  }
227  catch (const std::exception &e)
228  {
229  supervisor->log("Supervisor / intercepted thread exception: ", e.what());
230  }
231  break;
232 
233  default:
234  supervisor->log("Supervisor error: unknown exception handling type");
235  break;
236  }
237  }
238 
239 
240  template <class t_Logger, class t_Function, class... t_Args>
241  void startLoop(Supervisor<t_Logger> *supervisor, t_Function &&function, t_Args &&...args)
242  {
243  bool started = false;
244  for (std::size_t attempt = 0; parameters_.restart_.isOk(attempt) and not supervisor->isInterrupted();
245  ++attempt)
246  {
247  if (started)
248  {
250  if (supervisor->isInterrupted())
251  {
252  break;
253  }
254 
255  supervisor->log(
256  "Supervisor / restarting thread: ",
257  attempt + 1,
258  " / ",
260  }
261  else
262  {
263  started = true;
264  }
265 
266  startOnce(supervisor, function, std::forward<t_Args>(args)...);
267  }
268  }
269 
270 
271  template <class t_Logger, class t_Function, class... t_Args>
272  void startThread(Supervisor<t_Logger> *supervisor, t_Function &&function, t_Args &&...args)
273  {
275  {
276  startLoop(supervisor, function, std::forward<t_Args>(args)...);
277  }
278  else
279  {
280  startOnce(supervisor, function, std::forward<t_Args>(args)...);
281  }
282 
283 
285  {
287  supervisor->interrupt();
288  supervisor->drop(self_);
289  break;
290 
292  supervisor->drop(self_);
293  break;
294 
295  default:
296  supervisor->log("Supervisor error: unknown termination handling type");
297  break;
298  }
299  }
300 
301 
302  public:
303  template <class t_Logger, class... t_Args>
304  void start(
305  Supervisor<t_Logger> *supervisor,
306  Reference self,
307  const Parameters &&parameters,
308  t_Args &&...args)
309  {
310  self_ = self;
311  parameters_ = parameters;
312  thread_ = std::thread(
313  &Thread::startThread<t_Logger, t_Args...>, this, supervisor, std::forward<t_Args>(args)...);
314 
315  if (not parameters_.scheduling_.apply(thread_.native_handle()))
316  {
317  supervisor->log("Supervisor error: could not configure custom thread scheduling.");
319  {
320  std::terminate();
321  }
322  }
323  }
324 
325  void join()
326  {
327  if (thread_.joinable())
328  {
329  thread_.join();
330  }
331  }
332  };
333 
334 
335 
336  /// Thread supervisor
337  template <class t_Logger>
338  class Supervisor : public t_Logger
339  {
340  friend class Thread;
342 
343 
344  protected:
345  std::atomic<bool> interrupted_;
346 
347  std::mutex threads_mutex_;
349 
351  std::list<Thread::Reference> terminated_threads_;
352 
353 
354  protected:
355  bool wait(const std::size_t wait_ms)
356  {
357  const std::size_t sleep_ms = 10;
358 
359  {
360  const std::lock_guard<std::mutex> lock(threads_mutex_);
361  std::size_t counter = 0;
362  for (;;)
363  {
364  if (counter * sleep_ms < wait_ms)
365  {
366  {
367  const std::lock_guard<std::mutex> terminated_lock(terminated_threads_mutex_);
368 
369  for (const Thread::Reference &thread_ref : terminated_threads_)
370  {
371  thread_ref->join();
372  threads_.erase(thread_ref);
373  }
374  terminated_threads_.clear();
375  }
376 
377  if (not threads_.empty())
378  {
379  std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
380  ++counter;
381  }
382  else
383  {
384  return (true);
385  }
386  }
387  else
388  {
389  // cppcheck-suppress ignoredReturnValue
390  log("Threads did not terminate in the given time after request.");
391  return (false);
392  }
393  }
394  }
395  }
396 
397 
398  bool empty()
399  {
400  const std::lock_guard<std::mutex> lock(threads_mutex_);
401  return (threads_.empty());
402  }
403 
404 
405  void drop(const std::list<Thread>::iterator &item)
406  {
407  const std::lock_guard<std::mutex> lock(terminated_threads_mutex_);
408  terminated_threads_.push_back(item);
409  }
410 
411 
412  public:
413  using t_Logger::log;
414 
415 
417  {
418  interrupted_ = false;
419  }
420 
421 
423  {
424  if (not isInterrupted())
425  {
426  // - throwing in destructor -> terminate(), unless noexcept(false) is set.
427  // - noexcept(false) however interferes with other stuff in derived classes.
428  // - this check is intended to detect API misuse and should not fail under
429  // normal conditions.
430  // cppcheck-suppress ignoredReturnValue
431  log("Destructor of Supervisor is reached with 'interrupted_ = false',"
432  "which means that terminate() method was not called.");
433  std::terminate();
434  }
435 
436  if (not empty())
437  {
438  // cppcheck-suppress ignoredReturnValue
439  log("Destructor of Supervisor is reached with some threads still running.");
440  std::terminate();
441  }
442  }
443 
444 
445  /// Interrupt execution, see @ref isInterrupted
446  void interrupt()
447  {
448  interrupted_ = true;
449  }
450 
451 
452  /// Check if interrupted, child threads should stop when true (pass supervisor as a parameter).
453  [[nodiscard]] bool isInterrupted() const
454  {
455  return (interrupted_);
456  }
457 
458 
459  /// Interrupt and wait for children to stop
460  bool stop(const std::size_t wait_ms = 10000)
461  {
462  interrupt();
463  return (wait(wait_ms));
464  }
465 
466 
467  /// Add a thread: (<thread parameters>, <function pointer>, <function parameters>)
468  template <class... t_Args>
469  void add(t_Args &&...args)
470  {
471  if (isInterrupted())
472  {
473  // cppcheck-suppress ignoredReturnValue
474  log("Addition of a thread attempted after interrupt.");
475  std::terminate();
476  }
477  else
478  {
479  const std::lock_guard<std::mutex> lock(threads_mutex_);
480  threads_.emplace_back();
481  threads_.back().start(this, --threads_.end(), std::forward<t_Args>(args)...);
482  }
483  }
484  };
485 
486 
487 
488  /// Inheritable supervisor simplifies starting class methods in separate threads
489  template <class t_Logger = log::StdErr>
491  {
492  protected:
494 
495  protected:
497 
498  public:
499  [[nodiscard]] const Supervisor<t_Logger> &getThreadSupervisor() const
500  {
501  return (thread_supervisor_);
502  }
503 
505  {
506  return (thread_supervisor_);
507  }
508 
509 
510  /// should call stop(), pure virtual to remind that threads must be
511  /// destructed properly at the right time, e.g., from a destructor
512  virtual void stopSupervisedThreads() = 0;
513  /*
514  {
515  getThreadSupervisor().stop();
516  }
517  */
518 
519 
520  /// Add a thread: (<thread parameters>, <function pointer>, <this>, <function parameters>)
521  template <class... t_Args>
522  void addSupervisedThread(t_Args &&...args)
523  {
524  getThreadSupervisor().add(std::forward<t_Args>(args)...);
525  }
526 
527 
528  /// Should be checked by threaded class methods
529  [[nodiscard]] bool isThreadSupervisorInterrupted() const
530  {
531  return (getThreadSupervisor().isInterrupted());
532  }
533  };
534  } // namespace thread
535 } // namespace tut
void log() const
Definition: supervisor.h:44
void log(const t_Arg &arg, t_Args &&...args) const
Definition: supervisor.h:37
Inheritable supervisor simplifies starting class methods in separate threads.
Definition: supervisor.h:491
Supervisor< t_Logger > thread_supervisor_
Definition: supervisor.h:493
bool isThreadSupervisorInterrupted() const
Should be checked by threaded class methods.
Definition: supervisor.h:529
Supervisor< t_Logger > & getThreadSupervisor()
Definition: supervisor.h:504
const Supervisor< t_Logger > & getThreadSupervisor() const
Definition: supervisor.h:499
void addSupervisedThread(t_Args &&...args)
Add a thread: (<thread parameters>, <function pointer>, <this>, <function parameters>)
Definition: supervisor.h:522
virtual void stopSupervisedThreads()=0
Restarting parameters.
Definition: supervisor.h:75
Restart(const std::size_t attempts=0, const std::size_t sleep_ms=0)
delay between attempts
Definition: supervisor.h:81
bool isOk(const std::size_t attempt) const
Definition: supervisor.h:92
std::size_t sleep_ms_
0 = unlimited
Definition: supervisor.h:78
Scheduling parameters, POSIX threads only, see pthread_setschedparam.
Definition: supervisor.h:111
Scheduling(const int priority=0, const int policy=SCHED_FIFO, const bool ignore_failures=true)
proceed if scheduling parameters could not be set
Definition: supervisor.h:118
bool ignore_failures_
thread policy
Definition: supervisor.h:115
bool apply(const std::thread::native_handle_type &&handle) const
Definition: supervisor.h:129
Thread parameters.
Definition: supervisor.h:56
ExceptionPolicy exception_policy_
Definition: supervisor.h:155
TerminationPolicy termination_policy_
do not allow threads to exit / crash quietly
Definition: supervisor.h:154
Parameters(const ExceptionPolicy exception_policy, t_Args &&...args)
Definition: supervisor.h:182
Parameters(const Restart &&restart, t_Args &&...args)
Definition: supervisor.h:161
ExceptionPolicy
Exception handling policy.
Definition: supervisor.h:67
Parameters(const TerminationPolicy termination_policy, t_Args &&...args)
Definition: supervisor.h:175
TerminationPolicy
What to do when thread terminated.
Definition: supervisor.h:60
Parameters(const Scheduling &&scheduling, t_Args &&...args)
Definition: supervisor.h:168
Thread supervisor.
Definition: supervisor.h:339
THREAD_SUPERVISOR_DISABLE_CLASS_COPY(Supervisor) protected std::mutex threads_mutex_
Definition: supervisor.h:341
std::mutex terminated_threads_mutex_
Definition: supervisor.h:350
bool isInterrupted() const
Check if interrupted, child threads should stop when true (pass supervisor as a parameter).
Definition: supervisor.h:453
std::list< Thread::Reference > terminated_threads_
Definition: supervisor.h:351
void interrupt()
Interrupt execution, see isInterrupted.
Definition: supervisor.h:446
bool wait(const std::size_t wait_ms)
Definition: supervisor.h:355
Thread::List threads_
Definition: supervisor.h:348
bool stop(const std::size_t wait_ms=10000)
Interrupt and wait for children to stop.
Definition: supervisor.h:460
void drop(const std::list< Thread >::iterator &item)
Definition: supervisor.h:405
void add(t_Args &&...args)
Add a thread: (<thread parameters>, <function pointer>, <function parameters>)
Definition: supervisor.h:469
Thread handling class.
Definition: supervisor.h:199
List::iterator Reference
Definition: supervisor.h:204
std::list< Thread > List
Definition: supervisor.h:203
void startLoop(Supervisor< t_Logger > *supervisor, t_Function &&function, t_Args &&...args)
Definition: supervisor.h:241
void startThread(Supervisor< t_Logger > *supervisor, t_Function &&function, t_Args &&...args)
Definition: supervisor.h:272
void start(Supervisor< t_Logger > *supervisor, Reference self, const Parameters &&parameters, t_Args &&...args)
Definition: supervisor.h:304
Parameters parameters_
Definition: supervisor.h:207
std::thread thread_
Definition: supervisor.h:208
void startOnce(Supervisor< t_Logger > *supervisor, t_Function &&function, t_Args &&...args)
Definition: supervisor.h:214
Definition: supervisor.h:26
#define THREAD_SUPERVISOR_DISABLE_CLASS_COPY(Class)
Class definitions that prevent copying.
Definition: util.h:12