Use condvar for monitor sleep to allow instant shutdown wakeup

The monitor thread used nanosleep for its sample interval, which meant
the destructor had to block for up to one full sample period (100ms by
default) waiting for the monitor to wake up and notice _stop.

Replace nanosleep with pthread_cond_timedwait on a dedicated condvar
(_monitor_cond) using CLOCK_MONOTONIC.  The destructor now signals
the condvar immediately after setting _stop, so the monitor wakes up
and exits without waiting for the interval to expire.

This also eliminates the EINTR retry loop — pthread_cond_timedwait
handles signals internally and returns when either the timeout expires
or the condvar is signaled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-10 13:14:34 -04:00
parent a90e13a19d
commit 47a097ee96

View file

@ -173,9 +173,11 @@ private:
std::atomic<std::uint64_t> _tasks_completed{0};
// Auto-scaling state
bool _autoscale_enabled;
ScalingConfig _scaling_config;
pthread_t _monitor_thread;
bool _autoscale_enabled;
ScalingConfig _scaling_config;
pthread_t _monitor_thread;
pthread_mutex_t _monitor_mutex;
pthread_cond_t _monitor_cond;
std::atomic<std::uint64_t> _last_scale_time_usecs{0};
};
@ -200,8 +202,20 @@ ThreadPool::ThreadPool(const unsigned thread_count_,
_name(name_),
_autoscale_enabled(scaling_config_ != nullptr),
_scaling_config(),
_monitor_thread()
_monitor_thread(),
_monitor_mutex(PTHREAD_MUTEX_INITIALIZER),
_monitor_cond()
{
// Use CLOCK_MONOTONIC for the monitor condvar so timed waits
// are immune to wall-clock adjustments (NTP, settimeofday).
{
pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setclock(&attr,CLOCK_MONOTONIC);
pthread_cond_init(&_monitor_cond,&attr);
pthread_condattr_destroy(&attr);
}
if(scaling_config_)
_scaling_config = *scaling_config_;
@ -298,12 +312,19 @@ ThreadPool::~ThreadPool()
{
_stop.store(true,std::memory_order_release);
// Stop monitor thread first
// Wake the monitor thread so it exits immediately instead of
// sleeping for up to one sample interval.
if(_autoscale_enabled)
{
pthread_mutex_lock(&_monitor_mutex);
pthread_cond_signal(&_monitor_cond);
pthread_mutex_unlock(&_monitor_mutex);
pthread_join(_monitor_thread,NULL);
}
pthread_cond_destroy(&_monitor_cond);
pthread_mutex_destroy(&_monitor_mutex);
// Snapshot threads under lock to close the race with _remove_self.
// After _stop is set, _remove_self becomes a no-op, so no thread
// will detach or erase itself from this point forward.
@ -518,20 +539,26 @@ ThreadPool::monitor_routine(void *arg_)
std::size_t expected_count = btp->thread_count();
bool acted_last_cycle = false;
const struct timespec sleep_ts = {
static_cast<time_t>(interval / 1000000),
static_cast<long>((interval % 1000000) * 1000)
};
while(!btp->_stop.load(std::memory_order_acquire))
{
// Sleep for one sample interval, but wake immediately if the
// destructor signals _monitor_cond.
{
struct timespec remaining = sleep_ts;
while(nanosleep(&remaining,&remaining) == -1 && errno == EINTR)
struct timespec abs_ts;
clock_gettime(CLOCK_MONOTONIC,&abs_ts);
abs_ts.tv_nsec += (interval % 1000000) * 1000;
abs_ts.tv_sec += interval / 1000000;
if(abs_ts.tv_nsec >= 1000000000L)
{
if(btp->_stop.load(std::memory_order_acquire))
break;
abs_ts.tv_sec += 1;
abs_ts.tv_nsec -= 1000000000L;
}
pthread_mutex_lock(&btp->_monitor_mutex);
pthread_cond_timedwait(&btp->_monitor_cond,
&btp->_monitor_mutex,
&abs_ts);
pthread_mutex_unlock(&btp->_monitor_mutex);
}
if(btp->_stop.load(std::memory_order_acquire))