On Thursday, February 9, 2023 at 9:24:36 AM UTC, Frederick Virchanza Gotham wrote:
>
> If I write code for a worker thread that never ever ever should take more than 5 seconds to do its job,
> then killing it at 10 seconds, discarding its data and releasing its locks is better than allowing it to keep
> the lock and keep the entire program frozen.
I did a little more work on it. You can see it on GodBolt here:
https://godbolt.org/z/8G6s6Y3dx
and also I've copy-pasted it:
#include <cassert> // assert
#include <cstdint> // uint_fast32_t
#include <atomic> // atomic_flag, atomic<T>
#include <mutex> // mutex, condition_variable
#include <chrono> // steady_clock, milliseconds, years
#include <condition_variable> // condition_variable
#include <pthread.h> // pthread_t, pthread_equal, pthread_self
#include <signal.h> // pthread_kill
class timeout_mutex {
// Flag to be used as the main lock (like a mutex)
std::atomic_flag flag = ATOMIC_FLAG_INIT;
// Combination of mutex and condition_variable
// for waiting for a specific time interval
std::mutex mtx_for_cv{};
std::condition_variable cv{};
// Information about the currently-held lock
std::atomic<pthread_t> id{};
std::atomic<std::chrono::time_point<std::chrono::steady_clock>> t =
std::chrono::steady_clock::now() + std::chrono::years(65535u);
// A flag to be used as a lock to ensure that two threads
// don't simultaneously try to kill another thread
std::atomic_flag flag_for_killing = ATOMIC_FLAG_INIT;
public:
void lock_for_max(std::uint_fast32_t const millis) noexcept(false)
{
for (;;)
{
using namespace std::chrono;
if ( false == flag.test_and_set() ) // Try to acquire main lock
{
id = pthread_self();
t = steady_clock::now() + milliseconds(millis);
return;
}
// If control reaches here, we failed to acquire the lock
// So now we wait until one of two things happen:
// 1) The lock becomes available
// 2) The timeout expires
{ // Braces to ensure reduced scope for objects within
std::unique_lock<std::mutex> mylock(mtx_for_cv);
cv.wait_until(mylock,t.load()); // might get spurious wake-up here
}
if ( t.load() >= steady_clock::now() ) continue; // No killing before timeout expired
if ( false == flag.test() ) continue; // No killing if lock has become available
// If control reaches here, we should kill the thread,
// however two threads might simultaneously reach this
// point, so we need another atomic_flag as a lock
if ( false == flag_for_killing.test_and_set() )
{
// One last check that main lock hasn't been released
if ( flag.test() )
{
pthread_kill(id,SIGKILL); // or maybe pthread_cancel ?
// maybe put a delay here and check that the thread is dead
flag.clear();
}
flag_for_killing.clear();
}
}
}
void unlock(void) noexcept
{
// The calling thread might be in the middle
// of being killed right now, so acquire
// the 'killing lock' before proceeding
if ( flag_for_killing.test_and_set() ) return;
assert( pthread_equal(pthread_self(),id) ); // Ensure current thread is the one that locked it
assert( flag.test() ); // Ensure lock is still locked
using namespace std::chrono;
t = steady_clock::now() + years(65535u);
id = {};
flag_for_killing.clear();
flag.clear();
cv.notify_all();
}
};
int main(void)
{
}