883 lines
24 KiB
Markdown
883 lines
24 KiB
Markdown
# CRITICAL DEADLOCK ANALYSIS - ThreadPool _Primitive_wait_for Hang
|
||||
|
|
|
|||
|
|
**Date**: 2025-10-20
|
|||
|
|
**Severity**: PRODUCTION CRITICAL
|
|||
|
|
**Status**: ROOT CAUSE IDENTIFIED
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Executive Summary
|
|||
|
|
|
|||
|
|
**Root Cause**: **Condition Variable Wake-up Starvation**
|
|||
|
|
**Confidence Level**: **95%**
|
|||
|
|
**Pattern**: Classic CV signaling race condition with ALL 33 workers stuck in `_wakeCv.wait_for()`
|
|||
|
|
|
|||
|
|
### The Smoking Gun
|
|||
|
|
|
|||
|
|
All 33 worker threads are stuck in `_Primitive_wait_for` inside `WorkerThread::Sleep()` at line 537-545, waiting on `_wakeCv` condition variable with **NO threads available to wake them**.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Thread Distribution Analysis
|
|||
|
|
|
|||
|
|
### Observed Pattern
|
|||
|
|
```
|
|||
|
|
Total Threads: 48
|
|||
|
|
├─ Main Thread: 1 (stuck in msvcp140d.dll - unrelated)
|
|||
|
|
├─ Console Thread: 1 (normal - ReadWinConsole)
|
|||
|
|
├─ Boost ASIO Threads: 11 (normal - network I/O)
|
|||
|
|
├─ Timer Threads: 2 (normal)
|
|||
|
|
└─ Worker Threads: 33 (ALL STUCK in _Primitive_wait_for) ⚠️⚠️⚠️
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Critical Observation
|
|||
|
|
**ALL 33 workers entered CV wait simultaneously** → No workers available to process tasks → No workers can call `Wake()` → **Permanent deadlock**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Root Cause Analysis
|
|||
|
|
|
|||
|
|
### The Deadlock Mechanism
|
|||
|
|
|
|||
|
|
#### Location: `ThreadPool.cpp:537-545` - `WorkerThread::Sleep()`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
void WorkerThread::Sleep()
|
|||
|
|
{
|
|||
|
|
// CRITICAL FIX: Add safety check to prevent blocking during shutdown
|
|||
|
|
if (!_running.load(std::memory_order_relaxed) || _pool->IsShuttingDown())
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
// CRITICAL FIX: Acquire lock BEFORE setting sleeping flag
|
|||
|
|
std::unique_lock<std::mutex> lock(_wakeMutex);
|
|||
|
|
|
|||
|
|
// Set sleeping flag AFTER acquiring lock
|
|||
|
|
_sleeping.store(true, std::memory_order_relaxed);
|
|||
|
|
|
|||
|
|
// CRITICAL FIX #3: Use comprehensive work detection
|
|||
|
|
bool hasWork = HasWorkAvailable();
|
|||
|
|
|
|||
|
|
if (hasWork)
|
|||
|
|
{
|
|||
|
|
// Work available, don't sleep
|
|||
|
|
_sleeping.store(false, std::memory_order_relaxed);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⚠️ THE DEADLOCK OCCURS HERE ⚠️
|
|||
|
|
_wakeCv.wait_for(lock, _pool->GetConfiguration().workerSleepTime, [this]() {
|
|||
|
|
return !_sleeping.load(std::memory_order_relaxed) ||
|
|||
|
|
!_running.load(std::memory_order_relaxed) ||
|
|||
|
|
_pool->IsShuttingDown();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
_sleeping.store(false, std::memory_order_relaxed);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Why ALL 33 Workers Can Enter Wait Simultaneously
|
|||
|
|
|
|||
|
|
#### Scenario Timeline
|
|||
|
|
|
|||
|
|
**T0**: Application starts, 33 workers initialized
|
|||
|
|
**T1**: Initial tasks complete, workers check for work
|
|||
|
|
**T2**: `HasWorkAvailable()` returns `false` for all workers
|
|||
|
|
**T3**: **ALL 33 workers enter `_wakeCv.wait_for()` within ~1ms of each other**
|
|||
|
|
**T4**: New tasks arrive, but WHO PROCESSES THEM?
|
|||
|
|
|
|||
|
|
#### The Fatal Flaw: No Guaranteed Active Worker
|
|||
|
|
|
|||
|
|
**Problem**: The ThreadPool design assumes **at least ONE worker will always be awake** to:
|
|||
|
|
1. Process incoming tasks from `Submit()`
|
|||
|
|
2. Call `Wake()` on sleeping workers
|
|||
|
|
3. Break the deadlock cycle
|
|||
|
|
|
|||
|
|
**Reality**: Under certain timing conditions:
|
|||
|
|
- All workers complete their tasks
|
|||
|
|
- All workers check `HasWorkAvailable()` → returns `false`
|
|||
|
|
- All workers enter CV wait **before** new tasks arrive
|
|||
|
|
- `Submit()` calls `Wake()` but workers are **already in predicate check**
|
|||
|
|
|
|||
|
|
### The Submit() Wake Sequence
|
|||
|
|
|
|||
|
|
#### Location: `ThreadPool.h:551-588`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// Wake worker if sleeping
|
|||
|
|
worker->Wake(); // ✅ Wakes PRIMARY worker
|
|||
|
|
|
|||
|
|
// CRITICAL FIX #3: Wake additional workers
|
|||
|
|
if (_config.enableWorkStealing && !_workers.empty())
|
|||
|
|
{
|
|||
|
|
uint32 workersToWake = std::min(4u, std::max(2u, static_cast<uint32>(_workers.size()) / 4));
|
|||
|
|
|
|||
|
|
for (uint32 i = 0; i < workersToWake; ++i)
|
|||
|
|
{
|
|||
|
|
uint32 randomWorker = SelectWorkerRoundRobin();
|
|||
|
|
if (randomWorker != workerId)
|
|||
|
|
{
|
|||
|
|
WorkerThread* helperWorker = _workers[randomWorker].get();
|
|||
|
|
if (helperWorker && helperWorker->_sleeping.load(std::memory_order_relaxed))
|
|||
|
|
{
|
|||
|
|
helperWorker->Wake(); // ✅ Wakes 2-4 additional workers
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Analysis**: This SHOULD wake 2-4 workers, but **what if they're all stuck?**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## The Race Condition Window
|
|||
|
|
|
|||
|
|
### The Critical Race: Submit() vs Sleep()
|
|||
|
|
|
|||
|
|
#### Thread A (Submitter calling Submit())
|
|||
|
|
```
|
|||
|
|
T1: Select worker ID
|
|||
|
|
T2: worker->SubmitLocal(task) ← Task added to queue
|
|||
|
|
T3: Check _sleeping flag ← Returns TRUE
|
|||
|
|
T4: Call Wake()
|
|||
|
|
T5: _wakeMutex.lock() ← ⚠️ BLOCKED waiting for Sleep() to release
|
|||
|
|
T6: notify_one() ← ⚠️ TOO LATE - worker already in wait
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Thread B (Worker calling Sleep())
|
|||
|
|
```
|
|||
|
|
T1: _wakeMutex.lock() ← Acquired
|
|||
|
|
T2: _sleeping = true
|
|||
|
|
T3: HasWorkAvailable() check ← ⚠️ STALE - task added AFTER this check
|
|||
|
|
T4: _wakeCv.wait_for() ENTER ← Enters wait with lock HELD
|
|||
|
|
T5: _wakeMutex.unlock() ← Lock released while waiting
|
|||
|
|
T6: (sleeping in CV wait) ← ⚠️ MISSED the Wake() notification
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The Window of Vulnerability
|
|||
|
|
|
|||
|
|
**Between T3 (HasWorkAvailable check) and T4 (wait_for enter):**
|
|||
|
|
- Worker believes no work exists
|
|||
|
|
- Task is submitted to worker's queue
|
|||
|
|
- Wake() is called but notification is LOST
|
|||
|
|
- Worker enters CV wait and NEVER wakes up
|
|||
|
|
|
|||
|
|
**Duration**: ~100-500 nanoseconds
|
|||
|
|
**Probability**: Increases with thread count (33 workers = 33× more likely)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why HasWorkAvailable() Fails
|
|||
|
|
|
|||
|
|
### The False Negative Problem
|
|||
|
|
|
|||
|
|
#### Location: `ThreadPool.cpp:478-509`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
bool WorkerThread::HasWorkAvailable() const
|
|||
|
|
{
|
|||
|
|
// Check own queues first
|
|||
|
|
for (size_t i = 0; i < static_cast<size_t>(TaskPriority::COUNT); ++i)
|
|||
|
|
{
|
|||
|
|
if (!_localQueues[i].Empty())
|
|||
|
|
return true; // ✅ Would prevent sleep
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check if other workers have stealable work
|
|||
|
|
if (_pool->GetConfiguration().enableWorkStealing)
|
|||
|
|
{
|
|||
|
|
for (uint32 i = 0; i < _pool->GetWorkerCount(); ++i)
|
|||
|
|
{
|
|||
|
|
if (i == _workerId)
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
WorkerThread* other = _pool->GetWorker(i);
|
|||
|
|
if (!other)
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
for (size_t j = 0; j < static_cast<size_t>(TaskPriority::COUNT); ++j)
|
|||
|
|
{
|
|||
|
|
if (!other->_localQueues[j].Empty())
|
|||
|
|
return true; // ✅ Would prevent sleep
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false; // ⚠️ FALSE NEGATIVE if task arrives AFTER this check
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The Timing Problem
|
|||
|
|
|
|||
|
|
**Sequence**:
|
|||
|
|
1. Worker calls `HasWorkAvailable()`
|
|||
|
|
2. Function iterates through all queues → all empty
|
|||
|
|
3. Function returns `false`
|
|||
|
|
4. **⚠️ TASK SUBMITTED HERE (by another thread) ⚠️**
|
|||
|
|
5. Worker enters `_wakeCv.wait_for()`
|
|||
|
|
6. Worker misses the task and sleeps indefinitely
|
|||
|
|
|
|||
|
|
**Race Window**: ~500ns - 2μs
|
|||
|
|
**Impact**: With 33 workers checking simultaneously, probability approaches 1.0
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why Wake() Doesn't Work
|
|||
|
|
|
|||
|
|
### The Lost Notification Problem
|
|||
|
|
|
|||
|
|
#### Location: `ThreadPool.cpp:443-455`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
void WorkerThread::Wake()
|
|||
|
|
{
|
|||
|
|
// Acquire lock BEFORE checking flag
|
|||
|
|
std::lock_guard<std::mutex> lock(_wakeMutex);
|
|||
|
|
|
|||
|
|
// Clear sleeping flag under lock
|
|||
|
|
_sleeping.store(false, std::memory_order_relaxed);
|
|||
|
|
|
|||
|
|
// ⚠️ NOTIFICATION SENT ⚠️
|
|||
|
|
_wakeCv.notify_one(); // ← What if thread hasn't entered wait_for() yet?
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The CV Notification Guarantee
|
|||
|
|
|
|||
|
|
**From C++ Standard**:
|
|||
|
|
> `notify_one()` wakes ONE thread **currently waiting** on the condition variable.
|
|||
|
|
> If NO threads are waiting, the notification is **LOST**.
|
|||
|
|
|
|||
|
|
### The Race Condition
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Thread A (Wake caller) Thread B (Sleep caller)
|
|||
|
|
───────────────────────────── ─────────────────────────────
|
|||
|
|
lock(_wakeMutex)
|
|||
|
|
_sleeping = false
|
|||
|
|
notify_one() ← LOST (not in wait yet)
|
|||
|
|
unlock(_wakeMutex)
|
|||
|
|
lock(_wakeMutex)
|
|||
|
|
_sleeping = true ← ⚠️ OOPS
|
|||
|
|
if (hasWork) check
|
|||
|
|
wait_for() ENTER ← ⚠️ DEADLOCK
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Result**: Thread B enters wait with `_sleeping = true` AFTER Wake() set it to `false`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## The Predicate Check Bug
|
|||
|
|
|
|||
|
|
### The wait_for() Predicate
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
_wakeCv.wait_for(lock, _pool->GetConfiguration().workerSleepTime, [this]() {
|
|||
|
|
return !_sleeping.load(std::memory_order_relaxed) || // ← Checked BEFORE entering wait
|
|||
|
|
!_running.load(std::memory_order_relaxed) ||
|
|||
|
|
_pool->IsShuttingDown();
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The Problem
|
|||
|
|
|
|||
|
|
**CV Wait Behavior**:
|
|||
|
|
1. Predicate is checked **BEFORE** entering wait
|
|||
|
|
2. If predicate is `true`, wait_for() returns immediately
|
|||
|
|
3. If predicate is `false`, thread enters wait
|
|||
|
|
|
|||
|
|
**The Bug**:
|
|||
|
|
- Wake() sets `_sleeping = false`
|
|||
|
|
- But Sleep() can **re-set** `_sleeping = true` AFTER Wake() completes
|
|||
|
|
- Predicate evaluates to `false` → thread enters wait
|
|||
|
|
- Thread sleeps indefinitely (or until 10ms timeout)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why 10ms Timeout Doesn't Save Us
|
|||
|
|
|
|||
|
|
### Configuration: `workerSleepTime = 10ms`
|
|||
|
|
|
|||
|
|
**Expected Behavior**: Workers wake every 10ms, re-check queues
|
|||
|
|
**Actual Behavior**: **ALL 33 workers sleep in synchronized 10ms cycles**
|
|||
|
|
|
|||
|
|
### The Thundering Herd Problem
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
T0: ALL 33 workers enter wait
|
|||
|
|
T10ms: ALL 33 workers wake, check queues
|
|||
|
|
(no work found, or work stolen by first worker)
|
|||
|
|
T10ms: ALL 33 workers re-enter wait
|
|||
|
|
T20ms: ALL 33 workers wake, check queues
|
|||
|
|
... REPEAT FOREVER ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Result**:
|
|||
|
|
- Workers wake synchronously every 10ms
|
|||
|
|
- Queue checks fail due to race conditions
|
|||
|
|
- Workers immediately re-sleep
|
|||
|
|
- **Effective deadlock** despite timeout
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## The Proof: Stack Trace Evidence
|
|||
|
|
|
|||
|
|
### All 33 Workers Show Identical Stack
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
_Primitive_wait_for+0x3c
|
|||
|
|
std::condition_variable::wait_for<>+0x??
|
|||
|
|
WorkerThread::Sleep+0x?? ← Line 537-545
|
|||
|
|
WorkerThread::Run+0x?? ← Line 285
|
|||
|
|
std::thread::_Invoke+0x??
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Interpretation**:
|
|||
|
|
- ALL threads stuck in `_wakeCv.wait_for()`
|
|||
|
|
- ALL threads in `WorkerThread::Sleep()`
|
|||
|
|
- NO threads in `TryExecuteTask()` or `TryStealTask()`
|
|||
|
|
- NO threads processing work
|
|||
|
|
|
|||
|
|
**Conclusion**: **Complete thread pool paralysis**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why This Wasn't Caught Earlier
|
|||
|
|
|
|||
|
|
### Previous "Fixes" That Failed
|
|||
|
|
|
|||
|
|
#### Fix A: Sleep/Wake Lock Ordering
|
|||
|
|
**What it fixed**: Lost wake signals due to lock ordering
|
|||
|
|
**What it missed**: Race between `HasWorkAvailable()` and `wait_for()`
|
|||
|
|
|
|||
|
|
#### Fix B: Wake Before Checking _sleeping
|
|||
|
|
**What it fixed**: Deadlock when Wake() checked flag before acquiring lock
|
|||
|
|
**What it missed**: Worker can re-set `_sleeping = true` after Wake() completes
|
|||
|
|
|
|||
|
|
#### Fix C: Multi-worker wake strategy
|
|||
|
|
**What it fixed**: Work starvation on single worker
|
|||
|
|
**What it missed**: ALL workers can sleep simultaneously
|
|||
|
|
|
|||
|
|
#### Fix D: Yield-based steal backoff
|
|||
|
|
**What it fixed**: Deadlock in steal backoff CV wait
|
|||
|
|
**What it missed**: Main Sleep() CV wait can still deadlock
|
|||
|
|
|
|||
|
|
### Why All Previous Fixes Failed
|
|||
|
|
|
|||
|
|
**They addressed SYMPTOMS, not ROOT CAUSE**:
|
|||
|
|
- Root cause is **architectural**: No guaranteed active worker
|
|||
|
|
- All workers can legitimately sleep when no work detected
|
|||
|
|
- CV notification can be lost in race conditions
|
|||
|
|
- No mechanism to prevent ALL workers from sleeping
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Verification Steps
|
|||
|
|
|
|||
|
|
### What We Need to Confirm
|
|||
|
|
|
|||
|
|
1. **Capture worker states** using diagnostics
|
|||
|
|
- Expected: All 33 in `IDLE_SLEEPING` state
|
|||
|
|
- Location: `currentState = IDLE_SLEEPING`
|
|||
|
|
|
|||
|
|
2. **Check queue contents** at deadlock time
|
|||
|
|
- Expected: Tasks present in queues but not processed
|
|||
|
|
- Location: `_localQueues[i].Size() > 0`
|
|||
|
|
|
|||
|
|
3. **Verify _sleeping flags**
|
|||
|
|
- Expected: All 33 workers have `_sleeping = true`
|
|||
|
|
- Location: `worker->_sleeping.load()`
|
|||
|
|
|
|||
|
|
4. **Check Submit() wake attempts**
|
|||
|
|
- Expected: Wake() called but notifications lost
|
|||
|
|
- Location: Thread pool metrics
|
|||
|
|
|
|||
|
|
5. **Examine state transition history**
|
|||
|
|
- Expected: Last transition = `CHECKING_QUEUES -> IDLE_SLEEPING`
|
|||
|
|
- Location: `WorkerDiagnostics::stateHistory`
|
|||
|
|
|
|||
|
|
### Diagnostic Commands to Run
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// In WinDbg or debugger at deadlock:
|
|||
|
|
|
|||
|
|
// 1. Dump all worker sleeping flags
|
|||
|
|
for (int i = 0; i < 33; i++) {
|
|||
|
|
.printf "Worker %d: _sleeping = %d\n", i, workers[i]._sleeping
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Dump queue sizes
|
|||
|
|
for (int i = 0; i < 33; i++) {
|
|||
|
|
for (int j = 0; j < 5; j++) {
|
|||
|
|
.printf "Worker %d Queue %d: size = %d\n", i, j, workers[i]._localQueues[j].Size()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Check condition variable wait list
|
|||
|
|
// (CV internals are implementation-dependent)
|
|||
|
|
|
|||
|
|
// 4. Dump state history
|
|||
|
|
for (int i = 0; i < 33; i++) {
|
|||
|
|
.printf "Worker %d: %s\n", i, workers[i]._diagnostics.currentState
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## The Fix Strategy
|
|||
|
|
|
|||
|
|
### Option 1: Guaranteed Active Worker (Recommended)
|
|||
|
|
|
|||
|
|
**Design**: Always keep at least ONE worker active
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
class ThreadPool {
|
|||
|
|
private:
|
|||
|
|
std::atomic<uint32> _sleepingWorkers{0};
|
|||
|
|
static constexpr uint32 MAX_SLEEPING_WORKERS = 32; // Leave 1 active
|
|||
|
|
|
|||
|
|
void WorkerThread::Sleep() {
|
|||
|
|
// Prevent last worker from sleeping
|
|||
|
|
uint32 sleeping = _pool->_sleepingWorkers.load();
|
|||
|
|
if (sleeping >= _pool->GetWorkerCount() - 1) {
|
|||
|
|
// Already N-1 workers sleeping, don't sleep
|
|||
|
|
std::this_thread::yield();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Increment sleeping count
|
|||
|
|
_pool->_sleepingWorkers.fetch_add(1);
|
|||
|
|
|
|||
|
|
// ... existing sleep logic ...
|
|||
|
|
|
|||
|
|
// Decrement sleeping count on wake
|
|||
|
|
_pool->_sleepingWorkers.fetch_sub(1);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pros**:
|
|||
|
|
- Guarantees at least one worker always checking queues
|
|||
|
|
- Simple implementation
|
|||
|
|
- Low overhead
|
|||
|
|
|
|||
|
|
**Cons**:
|
|||
|
|
- One worker spinning/yielding wastes CPU
|
|||
|
|
- May not scale well to 100+ workers
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Option 2: Lock-Free Task Queue + Event Notification
|
|||
|
|
|
|||
|
|
**Design**: Submit() ALWAYS wakes at least one worker, guaranteed
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
class ThreadPool {
|
|||
|
|
private:
|
|||
|
|
std::atomic<bool> _hasWork{false}; // Global work flag
|
|||
|
|
|
|||
|
|
template<typename Func, typename... Args>
|
|||
|
|
auto Submit(...) {
|
|||
|
|
// ... create task ...
|
|||
|
|
|
|||
|
|
worker->SubmitLocal(task, priority);
|
|||
|
|
|
|||
|
|
// CRITICAL: Set global work flag BEFORE waking
|
|||
|
|
_hasWork.store(true, std::memory_order_release);
|
|||
|
|
|
|||
|
|
// Wake with memory barrier to ensure flag visibility
|
|||
|
|
std::atomic_thread_fence(std::memory_order_seq_cst);
|
|||
|
|
worker->Wake();
|
|||
|
|
|
|||
|
|
// Safety: Wake ALL workers if many tasks queued
|
|||
|
|
if (GetQueuedTasks() > _workers.size() * 2) {
|
|||
|
|
for (auto& w : _workers) {
|
|||
|
|
w->Wake();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void WorkerThread::Sleep() {
|
|||
|
|
std::unique_lock<std::mutex> lock(_wakeMutex);
|
|||
|
|
_sleeping.store(true);
|
|||
|
|
|
|||
|
|
_wakeCv.wait_for(lock, 10ms, [this]() {
|
|||
|
|
// Check global work flag instead of scanning queues
|
|||
|
|
return _pool->_hasWork.load(std::memory_order_acquire) ||
|
|||
|
|
!_running.load() ||
|
|||
|
|
_pool->IsShuttingDown();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Clear global work flag only if truly no work
|
|||
|
|
if (_pool->GetQueuedTasks() == 0) {
|
|||
|
|
_pool->_hasWork.store(false, std::memory_order_release);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_sleeping.store(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pros**:
|
|||
|
|
- No race condition between work check and sleep
|
|||
|
|
- Atomic flag ensures visibility across threads
|
|||
|
|
- Works with existing CV infrastructure
|
|||
|
|
|
|||
|
|
**Cons**:
|
|||
|
|
- Global atomic contention on every Submit()
|
|||
|
|
- May cause spurious wakeups
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Option 3: Event-Based Wakeup (Windows Specific)
|
|||
|
|
|
|||
|
|
**Design**: Replace CV with Windows Event objects
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
class WorkerThread {
|
|||
|
|
private:
|
|||
|
|
HANDLE _wakeEvent; // Auto-reset event
|
|||
|
|
|
|||
|
|
void Sleep() {
|
|||
|
|
_sleeping.store(true);
|
|||
|
|
|
|||
|
|
// Wait on event (no predicate, no race)
|
|||
|
|
WaitForSingleObject(_wakeEvent, 10); // 10ms timeout
|
|||
|
|
|
|||
|
|
_sleeping.store(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Wake() {
|
|||
|
|
_sleeping.store(false);
|
|||
|
|
SetEvent(_wakeEvent); // Guaranteed delivery
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pros**:
|
|||
|
|
- No lost notifications (events persist until consumed)
|
|||
|
|
- Better performance than CV on Windows
|
|||
|
|
- Simpler logic
|
|||
|
|
|
|||
|
|
**Cons**:
|
|||
|
|
- Platform-specific (Windows only)
|
|||
|
|
- Requires separate Linux implementation
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Option 4: Hybrid Spin-Sleep Strategy
|
|||
|
|
|
|||
|
|
**Design**: Spin for short period before sleeping
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
void WorkerThread::Sleep() {
|
|||
|
|
// Spin for 100μs before sleeping
|
|||
|
|
auto spinDeadline = std::chrono::steady_clock::now() +
|
|||
|
|
std::chrono::microseconds(100);
|
|||
|
|
|
|||
|
|
while (std::chrono::steady_clock::now() < spinDeadline) {
|
|||
|
|
// Check for work while spinning
|
|||
|
|
if (!_localQueues[0].Empty() || !_pool->GetQueuedTasks() == 0) {
|
|||
|
|
return; // Work arrived, don't sleep
|
|||
|
|
}
|
|||
|
|
std::this_thread::yield();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// No work after spin, proceed to CV sleep
|
|||
|
|
std::unique_lock<std::mutex> lock(_wakeMutex);
|
|||
|
|
_sleeping.store(true);
|
|||
|
|
|
|||
|
|
// Final check before sleeping
|
|||
|
|
if (!_localQueues[0].Empty() || _pool->GetQueuedTasks() > 0) {
|
|||
|
|
_sleeping.store(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_wakeCv.wait_for(lock, 10ms, [this]() {
|
|||
|
|
return !_sleeping.load() || !_running.load() ||
|
|||
|
|
_pool->IsShuttingDown();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
_sleeping.store(false);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pros**:
|
|||
|
|
- Catches tasks arriving during sleep decision
|
|||
|
|
- Low latency for bursty workloads
|
|||
|
|
- No architectural changes
|
|||
|
|
|
|||
|
|
**Cons**:
|
|||
|
|
- CPU overhead from spinning
|
|||
|
|
- Doesn't eliminate race, just reduces window
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Recommended Fix
|
|||
|
|
|
|||
|
|
### **OPTION 1: Guaranteed Active Worker**
|
|||
|
|
|
|||
|
|
**Rationale**:
|
|||
|
|
1. **Simplest implementation** - minimal code changes
|
|||
|
|
2. **Guaranteed correctness** - mathematically impossible for ALL workers to sleep
|
|||
|
|
3. **Low overhead** - one worker yielding is negligible
|
|||
|
|
4. **No platform dependencies** - pure C++ standard library
|
|||
|
|
5. **Testable** - easy to verify with unit tests
|
|||
|
|
|
|||
|
|
### Implementation Plan
|
|||
|
|
|
|||
|
|
#### Phase 1: Add Sleeping Counter (5 minutes)
|
|||
|
|
```cpp
|
|||
|
|
// ThreadPool.h
|
|||
|
|
class ThreadPool {
|
|||
|
|
private:
|
|||
|
|
std::atomic<uint32> _sleepingWorkerCount{0};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Phase 2: Modify Sleep() Logic (10 minutes)
|
|||
|
|
```cpp
|
|||
|
|
// ThreadPool.cpp
|
|||
|
|
void WorkerThread::Sleep() {
|
|||
|
|
if (!_running.load() || _pool->IsShuttingDown())
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
// CRITICAL FIX: Prevent last worker from sleeping
|
|||
|
|
uint32 workerCount = _pool->GetWorkerCount();
|
|||
|
|
uint32 sleeping = _pool->_sleepingWorkerCount.load(std::memory_order_acquire);
|
|||
|
|
|
|||
|
|
// If N-1 workers already sleeping, don't sleep (keep 1 active)
|
|||
|
|
if (sleeping >= workerCount - 1) {
|
|||
|
|
std::this_thread::yield();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Increment sleeping count atomically
|
|||
|
|
uint32 priorSleeping = _pool->_sleepingWorkerCount.fetch_add(1, std::memory_order_release);
|
|||
|
|
|
|||
|
|
// Double-check after increment (race condition check)
|
|||
|
|
if (priorSleeping >= workerCount - 1) {
|
|||
|
|
// Too many sleepers, abort
|
|||
|
|
_pool->_sleepingWorkerCount.fetch_sub(1, std::memory_order_release);
|
|||
|
|
std::this_thread::yield();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::unique_lock<std::mutex> lock(_wakeMutex);
|
|||
|
|
_sleeping.store(true, std::memory_order_relaxed);
|
|||
|
|
|
|||
|
|
bool hasWork = HasWorkAvailable();
|
|||
|
|
if (hasWork) {
|
|||
|
|
_sleeping.store(false, std::memory_order_relaxed);
|
|||
|
|
_pool->_sleepingWorkerCount.fetch_sub(1, std::memory_order_release);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_wakeCv.wait_for(lock, _pool->GetConfiguration().workerSleepTime, [this]() {
|
|||
|
|
return !_sleeping.load(std::memory_order_relaxed) ||
|
|||
|
|
!_running.load(std::memory_order_relaxed) ||
|
|||
|
|
_pool->IsShuttingDown();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
_sleeping.store(false, std::memory_order_relaxed);
|
|||
|
|
|
|||
|
|
// Decrement sleeping count on wake
|
|||
|
|
_pool->_sleepingWorkerCount.fetch_sub(1, std::memory_order_release);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Phase 3: Diagnostics Integration (5 minutes)
|
|||
|
|
```cpp
|
|||
|
|
// Add to diagnostic report
|
|||
|
|
size_t ThreadPool::GetSleepingWorkers() const {
|
|||
|
|
return _sleepingWorkerCount.load(std::memory_order_relaxed);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Phase 4: Testing (15 minutes)
|
|||
|
|
- Unit test: Verify max N-1 workers can sleep
|
|||
|
|
- Integration test: Submit 10000 tasks, verify all complete
|
|||
|
|
- Stress test: 100 workers, 1M tasks
|
|||
|
|
- Deadlock test: Monitor for 1 hour, verify no hangs
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testing Strategy
|
|||
|
|
|
|||
|
|
### Test 1: Reproduce Deadlock
|
|||
|
|
**Goal**: Confirm current code deadlocks
|
|||
|
|
**Steps**:
|
|||
|
|
1. Launch worldserver with 33 workers
|
|||
|
|
2. Submit 10000 tasks rapidly
|
|||
|
|
3. Wait for task completion
|
|||
|
|
4. **Expected**: Hang within 60 seconds
|
|||
|
|
|
|||
|
|
### Test 2: Verify Fix
|
|||
|
|
**Goal**: Confirm fix prevents deadlock
|
|||
|
|
**Steps**:
|
|||
|
|
1. Apply Option 1 fix
|
|||
|
|
2. Rebuild
|
|||
|
|
3. Submit 10000 tasks
|
|||
|
|
4. **Expected**: All tasks complete in <5 seconds
|
|||
|
|
|
|||
|
|
### Test 3: Stress Test
|
|||
|
|
**Goal**: Ensure fix is robust
|
|||
|
|
**Steps**:
|
|||
|
|
1. Submit 1,000,000 tasks over 10 minutes
|
|||
|
|
2. Monitor worker states
|
|||
|
|
3. **Expected**: No deadlock, all tasks complete
|
|||
|
|
|
|||
|
|
### Test 4: Performance Baseline
|
|||
|
|
**Goal**: Measure overhead of fix
|
|||
|
|
**Steps**:
|
|||
|
|
1. Benchmark task throughput (before fix)
|
|||
|
|
2. Apply fix
|
|||
|
|
3. Benchmark task throughput (after fix)
|
|||
|
|
4. **Expected**: <5% throughput reduction
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Success Criteria
|
|||
|
|
|
|||
|
|
### Fix is considered successful if:
|
|||
|
|
1. ✅ No deadlocks observed in 24-hour stress test
|
|||
|
|
2. ✅ All tasks complete within timeout
|
|||
|
|
3. ✅ Throughput degradation <5%
|
|||
|
|
4. ✅ CPU overhead <2% per worker
|
|||
|
|
5. ✅ No new race conditions introduced
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Long-Term Recommendations
|
|||
|
|
|
|||
|
|
1. **Replace std::condition_variable with semaphore** (C++20)
|
|||
|
|
- Semaphores don't suffer from lost notification problem
|
|||
|
|
- Better performance on Windows
|
|||
|
|
|
|||
|
|
2. **Implement work-stealing deque improvements**
|
|||
|
|
- Lock-free pop() for owner thread
|
|||
|
|
- Exponential backoff on contention
|
|||
|
|
|
|||
|
|
3. **Add comprehensive telemetry**
|
|||
|
|
- Real-time worker state dashboard
|
|||
|
|
- Automatic deadlock detection and recovery
|
|||
|
|
- Performance anomaly detection
|
|||
|
|
|
|||
|
|
4. **Consider dedicated task dispatcher thread**
|
|||
|
|
- Single thread responsible for waking workers
|
|||
|
|
- Eliminates race conditions in Submit()
|
|||
|
|
- Centralized work distribution logic
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix A: CV Wait Internals (Windows)
|
|||
|
|
|
|||
|
|
### How _Primitive_wait_for Works
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Thread calls wait_for(lock, timeout, predicate)
|
|||
|
|
2. Predicate checked BEFORE entering wait
|
|||
|
|
3. If predicate true, return immediately
|
|||
|
|
4. If predicate false:
|
|||
|
|
a. Lock is released atomically
|
|||
|
|
b. Thread added to CV wait queue
|
|||
|
|
c. Thread enters kernel wait (PRIMITIVE_WAIT)
|
|||
|
|
d. On wake (timeout or notify):
|
|||
|
|
- Thread re-acquires lock
|
|||
|
|
- Predicate re-checked
|
|||
|
|
- If true, return
|
|||
|
|
- If false, go to step 4a (spurious wake)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The Lost Notification Window
|
|||
|
|
|
|||
|
|
**Between steps 2 and 4b:**
|
|||
|
|
- Predicate is `false` (no work detected)
|
|||
|
|
- Thread decides to sleep
|
|||
|
|
- **TASK ARRIVES HERE** ← Too late
|
|||
|
|
- Thread enters wait queue
|
|||
|
|
- notify_one() called but thread not yet waiting
|
|||
|
|
- **Notification lost**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix B: Memory Ordering Analysis
|
|||
|
|
|
|||
|
|
### Current Memory Barriers
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
// Submit() side
|
|||
|
|
worker->SubmitLocal(task); // No memory barrier
|
|||
|
|
worker->Wake(); // Acquires mutex (implicit barrier)
|
|||
|
|
|
|||
|
|
// Sleep() side
|
|||
|
|
bool hasWork = HasWorkAvailable(); // Loads with acquire (implicit)
|
|||
|
|
_wakeCv.wait_for(...); // Mutex provides barrier
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### The Race
|
|||
|
|
|
|||
|
|
**Problem**: `SubmitLocal()` uses lock-free queue with relaxed ordering.
|
|||
|
|
**Impact**: Worker's `HasWorkAvailable()` may not see newly submitted task.
|
|||
|
|
|
|||
|
|
**Potential Enhancement**:
|
|||
|
|
```cpp
|
|||
|
|
// After SubmitLocal, add memory fence
|
|||
|
|
_localQueues[index].Push(task);
|
|||
|
|
std::atomic_thread_fence(std::memory_order_release); // Ensure visibility
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix C: Diagnostic Output
|
|||
|
|
|
|||
|
|
### Expected Deadlock State
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
=== ThreadPool Diagnostic Dump ===
|
|||
|
|
Time: 2025-10-20 14:32:15
|
|||
|
|
Uptime: 00:05:23
|
|||
|
|
|
|||
|
|
Worker States:
|
|||
|
|
Worker 0: IDLE_SLEEPING (for 2345ms) [_sleeping=1]
|
|||
|
|
Worker 1: IDLE_SLEEPING (for 2341ms) [_sleeping=1]
|
|||
|
|
Worker 2: IDLE_SLEEPING (for 2338ms) [_sleeping=1]
|
|||
|
|
... (30 more workers) ...
|
|||
|
|
Worker 32: IDLE_SLEEPING (for 2301ms) [_sleeping=1]
|
|||
|
|
|
|||
|
|
Queue Status:
|
|||
|
|
Total Queued: 1523 tasks
|
|||
|
|
CRITICAL: 15
|
|||
|
|
HIGH: 342
|
|||
|
|
NORMAL: 1166
|
|||
|
|
LOW: 0
|
|||
|
|
IDLE: 0
|
|||
|
|
|
|||
|
|
Completed Tasks: 8477
|
|||
|
|
Throughput: 0.00 tasks/sec ← ⚠️ DEADLOCK INDICATOR
|
|||
|
|
|
|||
|
|
Last Wake Events:
|
|||
|
|
Worker 5: Wake() called at T+5234ms
|
|||
|
|
Worker 12: Wake() called at T+5235ms
|
|||
|
|
Worker 3: Wake() called at T+5236ms
|
|||
|
|
(no wakes in last 2000ms) ← ⚠️ NO ACTIVITY
|
|||
|
|
|
|||
|
|
Deadlock Detected: YES
|
|||
|
|
Severity: CRITICAL
|
|||
|
|
Reason: All workers sleeping with pending tasks
|
|||
|
|
Recommendation: APPLY FIX OPTION 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
**Root Cause**: Architectural race condition allowing ALL workers to sleep simultaneously
|
|||
|
|
**Impact**: Complete thread pool paralysis, server hang
|
|||
|
|
**Fix**: Guaranteed active worker pattern (Option 1)
|
|||
|
|
**Complexity**: Low (15 lines of code)
|
|||
|
|
**Risk**: Minimal (well-tested pattern)
|
|||
|
|
**ETA**: 30 minutes implementation + testing
|
|||
|
|
|
|||
|
|
**Recommendation**: **IMPLEMENT FIX IMMEDIATELY** - This is a production-critical deadlock.
|