Library: thread-wrapper policy based design

Code written thus far was roughly complete and seems to work,
yet structure and life-cycle behaviour was confusing
This commit is contained in:
Fischlurch 2023-09-27 18:51:39 +02:00
parent 9c0fa7139d
commit 284984ad27
2 changed files with 118 additions and 86 deletions

View file

@ -31,11 +31,14 @@
#include "lib/thread.hpp"
#include "lib/format-string.hpp"
#include "lib/symbol.hpp"
#include "lib/util.hpp"
#include <chrono>
#include <pthread.h>
using util::_Fmt;
using lib::Literal;
using std::chrono::steady_clock;
using std::chrono_literals::operator ""ms;
@ -45,47 +48,49 @@ namespace lib {
namespace thread{
namespace {
void
setThreadName (std::thread& handle, string name)
string
lifecycleMsg (Literal phase, string threadID)
{
// API limitation: max 15 characters + \0
name = util::sanitise(name).substr(0, 15);
pthread_t nativeHandle = handle.native_handle();
pthread_setname_np(nativeHandle, name.c_str());
}
void
lifecycle (string format, string threadID)
{
string message = util::_Fmt{format} % threadID;
TRACE (thread, "%s", message.c_str());
return _Fmt{"Thread '%s' %s"} % threadID % phase;
}
}
template<bool autoTerm>
void
ThreadWrapper<autoTerm>::markThreadStart (string const& threadID)
/** @note implies get_id() != std::thread::id{} ==> it is running */
bool
ThreadWrapper::invokedWithinThread() const
{
lifecycle ("Thread '%s' start...", threadID);
setThreadName (threadImpl_, threadID);
return threadImpl_.get_id() == std::this_thread::get_id();
}
void
ThreadWrapper::markThreadStart()
{
TRACE (thread, "%s", lifecycleMsg ("start...", threadID_).c_str());
setThreadName();
}
template<bool autoTerm>
void
ThreadWrapper<autoTerm>::markThreadEnd (string const& threadID)
ThreadWrapper::markThreadEnd()
{
lifecycle ("Thread '%s' finished.", threadID);
TRACE (thread, "%s", lifecycleMsg ("finished.", threadID_).c_str());
}
template<bool autoTerm>
void
ThreadWrapper<autoTerm>::waitGracePeriod() noexcept
ThreadWrapper::setThreadName()
{
pthread_t nativeHandle = threadImpl_.native_handle();
// API limitation: max 15 characters + \0
pthread_setname_np(nativeHandle, threadID_.substr(0, 15).c_str());
}
void
ThreadWrapper::waitGracePeriod() noexcept
{
try {
auto start = steady_clock::now();
@ -94,21 +99,13 @@ namespace thread{
)
std::this_thread::yield();
}
ERROR_LOG_AND_IGNORE (thread, "Thread shutdown wait")
ERROR_LOG_AND_IGNORE (thread, lifecycleMsg("shutdown wait", threadID_).c_str());
if (threadImpl_.joinable())
ALERT (thread, "Thread failed to terminate after grace period. Abort.");
ALERT (thread, "Thread '%s' failed to terminate after grace period. Abort.", threadID_.c_str());
// invocation of std::thread dtor will presumably call std::terminate...
}
template void ThreadWrapper<true>::markThreadStart (string const&);
template void ThreadWrapper<true>::markThreadEnd (string const&);
template void ThreadWrapper<true>::waitGracePeriod () noexcept;
template void ThreadWrapper<false>::markThreadStart (string const&);
template void ThreadWrapper<false>::markThreadEnd (string const&);
template void ThreadWrapper<false>::waitGracePeriod () noexcept;
}}// namespace lib::thread

View file

@ -111,47 +111,108 @@
#include <utility>
namespace util {
std::string sanitise (std::string const&);
}
namespace lib {
using std::string;
namespace thread {// Thread-wrapper base implementation...
template<bool autoTerm>
class ThreadWrapper
struct ThreadWrapper
: util::MoveOnly
{
const string threadID_;
std::thread threadImpl_;
bool isLive() const { return threadImpl_.joinable(); }
/** @internal derived classes may create an inactive thread */
ThreadWrapper()
: threadID_{util::BOTTOM_INDICATOR}
, threadImpl_{}
{ }
template<typename...ARGS>
ThreadWrapper (string const& threadID, ARGS&& ...args)
: threadID_{util::sanitise (threadID)}
, threadImpl_{std::forward<ARGS> (args)... }
{ }
/** determine if the currently executing code runs within this thread */
bool invokedWithinThread() const;
void markThreadStart();
void markThreadEnd ();
void setThreadName ();
void waitGracePeriod() noexcept;
};
template<class BAS>
struct PolicyTODO
: BAS
{
using BAS::BAS;
void
handle_thread_still_running()
{
BAS::waitGracePeriod();
}
};
/**
* Policy-based configuration of thread lifecycle
*/
template<template<class> class POL>
class ThreadLifecycle
: protected POL<ThreadWrapper>
{
using Policy = POL<ThreadWrapper>;
template<class FUN, typename...ARGS>
void
main (string threadID, FUN&& threadFunction, ARGS&& ...args)
main (FUN&& threadFunction, ARGS&& ...args)
{
markThreadStart (threadID);
Policy::markThreadStart();
try {
// execute the actual operation in this new thread
std::invoke (std::forward<FUN> (threadFunction), std::forward<ARGS> (args)...);
}
ERROR_LOG_AND_IGNORE (thread, "Thread function")
//
markThreadEnd (threadID);
if (autoTerm)
threadImpl_.detach();
Policy::markThreadEnd();
// if (autoTerm)
Policy::threadImpl_.detach();
}
protected:
std::thread threadImpl_;
/** @internal derived classes may create an inactive thread */
ThreadWrapper() : threadImpl_{} { }
~ThreadWrapper()
~ThreadLifecycle()
{
if (autoTerm and threadImpl_.joinable())
waitGracePeriod();
if (Policy::isLive())
Policy::handle_thread_still_running();
}
public:
/**
* Is this thread »active« and thus tied to OS resources?
* @note this implies some statefulness, which may contradict the RAII pattern.
* - especially note the possibly for derived classes to create an _empty_ Thread.
* - moreover note that ThreadJoinable may have terminated, but still awaits `join()`.
*/
explicit
operator bool() const
{
return Policy::isLive();
}
/** Create a new thread to execute the given operation.
* The new thread starts up synchronously, can't be cancelled and it can't be joined.
* @param threadID human readable descriptor to identify the thread for diagnostics
@ -164,40 +225,14 @@ namespace lib {
* until the new thread terminates.
*/
template<class FUN, typename...ARGS>
ThreadWrapper (string const& threadID, FUN&& threadFunction, ARGS&& ...args)
: threadImpl_{&ThreadWrapper::main<FUN,ARGS...>, this
, threadID
, std::forward<FUN> (threadFunction)
, std::forward<ARGS> (args)... }
ThreadLifecycle (string const& threadID, FUN&& threadFunction, ARGS&& ...args)
: Policy{threadID
, &ThreadLifecycle::main<FUN,ARGS...>, this
, std::forward<FUN> (threadFunction)
, std::forward<ARGS> (args)... }
{ }
/**
* Is this thread »active« and thus tied to OS resources?
* @note this implies some statefulness, which may contradict the RAII pattern.
* - especially note the possibly for derived classes to create an _empty_ Thread.
* - moreover note that ThreadJoinable may have terminated, but still awaits `join()`.
*/
explicit
operator bool() const
{
return threadImpl_.joinable();
}
protected:
/** determine if the currently executing code runs within this thread */
bool
invokedWithinThread() const
{
return threadImpl_.get_id() == std::this_thread::get_id();
} // Note: implies get_id() != std::thread::id{} ==> it is running
private:
void markThreadStart (string const& threadID);
void markThreadEnd (string const& threadID);
void waitGracePeriod() noexcept;
};
}//(End)base implementation.
@ -215,11 +250,11 @@ namespace lib {
* `std::terminate` afterwards, should the thread still be active then.
*/
class Thread
: public thread::ThreadWrapper<true>
: public thread::ThreadLifecycle<thread::PolicyTODO>
{
public:
using ThreadWrapper::ThreadWrapper;
using ThreadLifecycle::ThreadLifecycle;
};
@ -237,10 +272,10 @@ namespace lib {
* the application is shut down immediately via `std::terminate`.
*/
class ThreadJoinable
: public thread::ThreadWrapper<false>
: public thread::ThreadLifecycle<thread::PolicyTODO>
{
public:
using ThreadWrapper::ThreadWrapper;
using ThreadLifecycle::ThreadLifecycle;
/** put the caller into a blocking wait until this thread has terminated */
void