thread_pool: serialize idle self-exit with scaling operations

_try_remove_self_if_above only held _threads_mutex, while set_threads
and the monitor thread hold _scaling_mutex.  An idle worker calling
_try_remove_self_if_above could shrink the pool concurrently with
set_threads reading _threads.size(), causing set_threads to
miscalculate the delta and overshoot (add too many or remove too few
threads).

Fix by acquiring _scaling_mutex before _threads_mutex in
_try_remove_self_if_above.  This matches the existing lock ordering
used by _maybe_grow_on_pressure and the monitor/set_threads paths
(_scaling_mutex always acquired before _threads_mutex), so no
deadlock is introduced.

Now all paths that change the thread count — add_thread,
remove_thread, set_threads, monitor hill-climb, pressure grow, and
idle self-exit — are serialized under _scaling_mutex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-10 16:46:01 -04:00
parent dbb7c5e785
commit 05413b86fd

View file

@ -721,11 +721,20 @@ ThreadPool::_remove_self(pthread_t t_)
// Atomically check thread count and remove self if above min_count_.
// Returns true if removed, false if at or below threshold.
//
// Acquires _scaling_mutex then _threads_mutex (matching the lock
// ordering used by _maybe_grow_on_pressure and the monitor) so that
// idle self-exit is serialized with set_threads, _remove_thread, and
// _add_thread. Without _scaling_mutex, an idle self-exit racing with
// set_threads could cause set_threads to overshoot because it reads
// _threads.size() under _threads_mutex but the idle exit shrinks the
// pool outside _scaling_mutex.
inline
bool
ThreadPool::_try_remove_self_if_above(pthread_t t_,
std::size_t min_count_)
{
mutex_lockguard(_scaling_mutex);
mutex_lockguard(_threads_mutex);
// Check _stop under the lock to close the TOCTOU race with the