fix data race on _threads between constructor and autoscale workers

The constructor's spawn loop pushed to _threads without holding
_threads_mutex.  A just-spawned autoscale worker could time out on
the empty queue (especially with a small idle_threshold_usecs) and
call _try_remove_self_if_above, which acquires _threads_mutex and
modifies _threads (find + erase).  Concurrent unlocked push_back
and locked erase on the same vector is undefined behavior per the
C++ memory model, even when reserve() prevents reallocation.

The same race applied to the _threads.empty() and _threads.size()
reads after the spawn loop.

Fix: lock _threads_mutex around push_back in the spawn loop and
around the post-loop size check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-10 22:24:14 -04:00
parent 26c10d8a2e
commit 81e8f6281a

View file

@ -290,23 +290,42 @@ ThreadPool::ThreadPool(const unsigned thread_count_,
}
_set_thread_name(t,_name);
_threads.push_back(t);
// Lock around push_back because a just-spawned autoscale
// worker can time out on the empty queue and call
// _try_remove_self_if_above (which acquires _threads_mutex
// and modifies _threads) before the constructor finishes.
// Concurrent unlocked push_back and locked erase is UB.
{
mutex_lockguard(_threads_mutex);
_threads.push_back(t);
}
}
pthread_sigmask(SIG_SETMASK,&oldset,NULL);
if(_threads.empty())
// Check under lock: a spawned autoscale worker could have
// already modified _threads via _try_remove_self_if_above.
{
std::size_t n;
{
pthread_cond_destroy(&_monitor_cond);
pthread_mutex_destroy(&_monitor_mutex);
throw std::runtime_error("threadpool: failed to spawn any threads");
mutex_lockguard(_threads_mutex);
n = _threads.size();
}
syslog(LOG_DEBUG,
"threadpool (%s): spawned %zu threads w/ max queue depth %u",
_name.c_str(),
_threads.size(),
max_queue_depth_);
if(n == 0)
{
pthread_cond_destroy(&_monitor_cond);
pthread_mutex_destroy(&_monitor_mutex);
throw std::runtime_error("threadpool: failed to spawn any threads");
}
syslog(LOG_DEBUG,
"threadpool (%s): spawned %zu threads w/ max queue depth %u",
_name.c_str(),
n,
max_queue_depth_);
}
// Start the monitor only when dynamic scaling can actually change
// the thread count. Fixed-size pools still use _scaling_config for