Skip to content

Commit bcb20c0

Browse files
committed
Fix priority queue bug
1 parent 545e92a commit bcb20c0

3 files changed

Lines changed: 98 additions & 57 deletions

File tree

‎README.md‎

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ When the timed wait expires with an empty queue, the loop simply continues back
9494

9595
**3. Dequeue and back pressure release**
9696

97-
Messages are dequeued with `m_queue.top()` / `m_queue.pop()`, which returns the highest-priority waiting message (`HIGH` before `NORMAL` before `LOW`). After removing a message, `m_cvNotFull` is signalled to wake any producer that blocked in `PostMsg()` because the queue was at its `MAX_QUEUE_SIZE` limit.
97+
Messages are dequeued from the first non-empty priority queue, checked in order `HIGH`, `NORMAL`, `LOW`. Each queue is a `std::deque` drained front-to-back, so messages posted at the same priority are always processed in the order they were posted. After removing a message, `m_cvNotFull` is signalled to wake any producer that blocked in `PostMsg()` because the queue was at its `MAX_QUEUE_SIZE` limit.
9898

9999
```cpp
100100
void Thread::Process()
@@ -111,34 +111,52 @@ void Thread::Process()
111111
{
112112
unique_lock<mutex> lk(m_mutex);
113113

114+
auto empty = [this]() {
115+
return m_highQueue.empty() && m_normalQueue.empty() && m_lowQueue.empty();
116+
};
117+
114118
if (m_watchdogTimeout.load() > steady_clock::duration::zero())
115119
{
116120
// Timed wait: wake periodically to keep m_lastAliveTime
117121
// current even when the queue is empty.
118122
auto heartbeat = m_watchdogTimeout.load() / 4;
119-
m_cv.wait_for(lk, heartbeat, [this]() {
120-
return !m_queue.empty() || m_exit.load();
123+
m_cv.wait_for(lk, heartbeat, [this, &empty]() {
124+
return !empty() || m_exit.load();
121125
});
122126
}
123127
else
124128
{
125129
// No watchdog: block until a message arrives or exit is set.
126-
m_cv.wait(lk, [this]() {
127-
return !m_queue.empty() || m_exit.load();
130+
m_cv.wait(lk, [this, &empty]() {
131+
return !empty() || m_exit.load();
128132
});
129133
}
130134

131-
if (m_queue.empty())
135+
if (empty())
132136
{
133137
// Either the heartbeat fired (loop back, refresh timestamp)
134138
// or ExitThread() was called with nothing left (exit).
135139
if (m_exit.load()) return;
136140
continue;
137141
}
138142

139-
// Dequeue highest-priority message (HIGH > NORMAL > LOW).
140-
msg = m_queue.top();
141-
m_queue.pop();
143+
// Dequeue from the highest-priority non-empty queue
144+
// (HIGH > NORMAL > LOW). Each queue is FIFO.
145+
if (!m_highQueue.empty())
146+
{
147+
msg = m_highQueue.front();
148+
m_highQueue.pop_front();
149+
}
150+
else if (!m_normalQueue.empty())
151+
{
152+
msg = m_normalQueue.front();
153+
m_normalQueue.pop_front();
154+
}
155+
else
156+
{
157+
msg = m_lowQueue.front();
158+
m_lowQueue.pop_front();
159+
}
142160

143161
// Wake a producer blocked on a full queue.
144162
if (MAX_QUEUE_SIZE > 0)
@@ -173,21 +191,30 @@ void Thread::PostMsg(std::shared_ptr<UserData> data, Priority priority)
173191

174192
unique_lock<mutex> lk(m_mutex);
175193

176-
if (MAX_QUEUE_SIZE > 0 && m_queue.size() >= MAX_QUEUE_SIZE)
194+
auto totalSize = [this]() {
195+
return m_highQueue.size() + m_normalQueue.size() + m_lowQueue.size();
196+
};
197+
198+
if (MAX_QUEUE_SIZE > 0 && totalSize() >= MAX_QUEUE_SIZE)
177199
{
178200
if (FULL_POLICY == FullPolicy::DROP)
179201
return; // silently discard — caller is not stalled
180202

181-
m_cvNotFull.wait(lk, [this]() {
182-
return m_queue.size() < MAX_QUEUE_SIZE || m_exit.load();
203+
m_cvNotFull.wait(lk, [this, &totalSize]() {
204+
return totalSize() < MAX_QUEUE_SIZE || m_exit.load();
183205
});
184206
}
185207

186208
if (m_exit.load())
187209
return;
188210

189211
auto threadMsg = make_shared<ThreadMsg>(MSG_POST_USER_DATA, data, priority);
190-
m_queue.push(threadMsg);
212+
switch (priority)
213+
{
214+
case Priority::HIGH: m_highQueue.push_back(threadMsg); break;
215+
case Priority::NORMAL: m_normalQueue.push_back(threadMsg); break;
216+
case Priority::LOW: m_lowQueue.push_back(threadMsg); break;
217+
}
191218
m_cv.notify_one();
192219
}
193220
```
@@ -196,25 +223,19 @@ void Thread::PostMsg(std::shared_ptr<UserData> data, Priority priority)
196223
197224
# Priority Queue
198225
199-
Messages are stored in a `std::priority_queue` rather than a plain `std::queue`. A custom comparator ensures messages with a higher `Priority` value are dequeued first.
226+
Messages are stored in one `std::deque` per priority level rather than a single combined queue:
200227
201228
```cpp
202229
enum class Priority { LOW = 0, NORMAL = 1, HIGH = 2 };
203230
204-
struct ThreadMsgComparator {
205-
bool operator()(const std::shared_ptr<ThreadMsg>& a,
206-
const std::shared_ptr<ThreadMsg>& b) const {
207-
return static_cast<int>(a->GetPriority()) < static_cast<int>(b->GetPriority());
208-
}
209-
};
210-
211-
std::priority_queue<
212-
std::shared_ptr<ThreadMsg>,
213-
std::vector<std::shared_ptr<ThreadMsg>>,
214-
ThreadMsgComparator> m_queue;
231+
std::deque<std::shared_ptr<ThreadMsg>> m_highQueue;
232+
std::deque<std::shared_ptr<ThreadMsg>> m_normalQueue;
233+
std::deque<std::shared_ptr<ThreadMsg>> m_lowQueue;
215234
```
216235

217-
When several messages are in the queue simultaneously, the worker thread always processes the `HIGH` priority message first, then `NORMAL`, then `LOW`, regardless of the order they were posted. This is useful for giving urgent work — such as a shutdown or error signal — preferential access to the thread without a separate fast-path queue.
236+
`PostMsg()` appends to the deque matching the message's priority; `Process()` dequeues from the first non-empty deque, checked in order `HIGH`, `NORMAL`, `LOW`. When several messages are queued simultaneously, the worker thread always processes `HIGH` priority messages first, then `NORMAL`, then `LOW`, regardless of post order across levels — useful for giving urgent work, such as a shutdown or error signal, preferential access to the thread without a separate fast-path queue. Because each level is its own FIFO deque, messages posted at the *same* priority are always processed in the order they were posted.
237+
238+
> An earlier version of this class used a single `std::priority_queue` with a comparator over `Priority`. That correctly ordered different priorities, but `std::priority_queue`'s underlying binary heap does not preserve insertion order among equal elements — messages posted at the same priority could be dequeued out of order. The per-priority deque design fixes this while keeping the same `HIGH` > `NORMAL` > `LOW` ordering guarantee across levels.
218239
219240
# Back Pressure
220241

‎Thread.cpp‎

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ void Thread::ExitThread()
9898
{
9999
lock_guard<mutex> lock(m_mutex);
100100
m_exit.store(true);
101-
m_queue.push(exitMsg);
101+
m_highQueue.push_back(exitMsg);
102102
m_cv.notify_one();
103103
m_cvNotFull.notify_all(); // unblock any blocked producers
104104
}
@@ -114,8 +114,9 @@ void Thread::ExitThread()
114114
{
115115
lock_guard<mutex> lock(m_mutex);
116116
m_thread.reset();
117-
while (!m_queue.empty())
118-
m_queue.pop();
117+
m_highQueue.clear();
118+
m_normalQueue.clear();
119+
m_lowQueue.clear();
119120
m_cvNotFull.notify_all();
120121
}
121122
}
@@ -153,7 +154,7 @@ bool Thread::IsCurrentThread()
153154
size_t Thread::GetQueueSize()
154155
{
155156
lock_guard<mutex> lock(m_mutex);
156-
return m_queue.size();
157+
return m_highQueue.size() + m_normalQueue.size() + m_lowQueue.size();
157158
}
158159

159160
//----------------------------------------------------------------------------
@@ -180,23 +181,32 @@ void Thread::PostMsg(std::shared_ptr<UserData> data, Priority priority)
180181

181182
unique_lock<mutex> lk(m_mutex);
182183

184+
auto totalSize = [this]() {
185+
return m_highQueue.size() + m_normalQueue.size() + m_lowQueue.size();
186+
};
187+
183188
// [BACK PRESSURE / DROP LOGIC]
184-
if (MAX_QUEUE_SIZE > 0 && m_queue.size() >= MAX_QUEUE_SIZE)
189+
if (MAX_QUEUE_SIZE > 0 && totalSize() >= MAX_QUEUE_SIZE)
185190
{
186191
if (FULL_POLICY == FullPolicy::DROP)
187192
return; // silently discard — caller is not stalled
188193

189194
// BLOCK: wait until the consumer drains a slot or the thread exits
190-
m_cvNotFull.wait(lk, [this]() {
191-
return m_queue.size() < MAX_QUEUE_SIZE || m_exit.load();
195+
m_cvNotFull.wait(lk, [this, &totalSize]() {
196+
return totalSize() < MAX_QUEUE_SIZE || m_exit.load();
192197
});
193198
}
194199

195200
if (m_exit.load())
196201
return;
197202

198203
auto threadMsg = make_shared<ThreadMsg>(MSG_POST_USER_DATA, data, priority);
199-
m_queue.push(threadMsg);
204+
switch (priority)
205+
{
206+
case Priority::HIGH: m_highQueue.push_back(threadMsg); break;
207+
case Priority::NORMAL: m_normalQueue.push_back(threadMsg); break;
208+
case Priority::LOW: m_lowQueue.push_back(threadMsg); break;
209+
}
200210
m_cv.notify_one();
201211
}
202212

@@ -281,19 +291,19 @@ void Thread::Process()
281291
// watchdog would incorrectly report the thread as unresponsive.
282292
auto heartbeat = m_watchdogTimeout.load() / 4;
283293
m_cv.wait_for(lk, heartbeat, [this]() {
284-
return !m_queue.empty() || m_exit.load();
294+
return !(m_highQueue.empty() && m_normalQueue.empty() && m_lowQueue.empty()) || m_exit.load();
285295
});
286296
}
287297
else
288298
{
289299
// No watchdog: block indefinitely until a message arrives or
290300
// ExitThread() sets m_exit and notifies.
291301
m_cv.wait(lk, [this]() {
292-
return !m_queue.empty() || m_exit.load();
302+
return !(m_highQueue.empty() && m_normalQueue.empty() && m_lowQueue.empty()) || m_exit.load();
293303
});
294304
}
295305

296-
if (m_queue.empty())
306+
if (m_highQueue.empty() && m_normalQueue.empty() && m_lowQueue.empty())
297307
{
298308
// Woken with no message — either the watchdog heartbeat fired
299309
// (loop back to refresh m_lastAliveTime) or ExitThread() was
@@ -302,11 +312,24 @@ void Thread::Process()
302312
continue;
303313
}
304314

305-
// Dequeue the highest-priority waiting message.
306-
// std::priority_queue::top() returns the greatest element per the
307-
// ThreadMsgComparator, i.e. HIGH > NORMAL > LOW.
308-
msg = m_queue.top();
309-
m_queue.pop();
315+
// Dequeue the oldest waiting message from the highest-priority
316+
// non-empty queue: HIGH before NORMAL before LOW. Each queue is
317+
// FIFO, so same-priority messages are processed in post order.
318+
if (!m_highQueue.empty())
319+
{
320+
msg = m_highQueue.front();
321+
m_highQueue.pop_front();
322+
}
323+
else if (!m_normalQueue.empty())
324+
{
325+
msg = m_normalQueue.front();
326+
m_normalQueue.pop_front();
327+
}
328+
else
329+
{
330+
msg = m_lowQueue.front();
331+
m_lowQueue.pop_front();
332+
}
310333

311334
// --- Back pressure: notify a blocked producer -----------------
312335
// A producer in PostMsg() may be sleeping on m_cvNotFull because

‎Thread.h‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
///
1111
/// @details
1212
/// Key features:
13-
/// * **Priority Queue:** Uses std::priority_queue so high-priority messages
14-
/// are processed before lower-priority ones.
13+
/// * **Priority Queue:** Uses one FIFO deque per priority level (HIGH,
14+
/// NORMAL, LOW) so higher-priority messages are always processed before
15+
/// lower-priority ones, while messages posted at the same priority are
16+
/// processed in the order they were posted. (An earlier implementation
17+
/// used a single std::priority_queue, whose underlying binary heap does
18+
/// not preserve insertion order among equal-priority elements.)
1519
/// * **Back Pressure:** Configurable maxQueueSize. When the queue is full,
1620
/// PostMsg() blocks the caller until space is available.
1721
/// * **Watchdog:** Optional timeout detects a stalled thread (deadlock or
@@ -22,7 +26,7 @@
2226

2327
#include "ThreadMsg.h"
2428
#include <thread>
25-
#include <queue>
29+
#include <deque>
2630
#include <mutex>
2731
#include <atomic>
2832
#include <condition_variable>
@@ -37,14 +41,6 @@ struct UserData
3741
int year;
3842
};
3943

40-
// Comparator: highest Priority value is processed first
41-
struct ThreadMsgComparator {
42-
bool operator()(const std::shared_ptr<ThreadMsg>& a,
43-
const std::shared_ptr<ThreadMsg>& b) const {
44-
return static_cast<int>(a->GetPriority()) < static_cast<int>(b->GetPriority());
45-
}
46-
};
47-
4844
/// @brief Policy applied when the thread message queue is full.
4945
/// @details Only meaningful when maxQueueSize > 0.
5046
/// - BLOCK: PostMsg() blocks the caller until space is available (back pressure).
@@ -115,10 +111,11 @@ class Thread
115111
std::optional<std::thread> m_thread;
116112
std::atomic<bool> m_exit;
117113

118-
std::priority_queue<
119-
std::shared_ptr<ThreadMsg>,
120-
std::vector<std::shared_ptr<ThreadMsg>>,
121-
ThreadMsgComparator> m_queue;
114+
// One FIFO queue per priority level. Draining checks HIGH, then NORMAL,
115+
// then LOW, so within a level messages are processed in post order.
116+
std::deque<std::shared_ptr<ThreadMsg>> m_highQueue;
117+
std::deque<std::shared_ptr<ThreadMsg>> m_normalQueue;
118+
std::deque<std::shared_ptr<ThreadMsg>> m_lowQueue;
122119

123120
std::mutex m_mutex;
124121
std::condition_variable m_cv; // notifies consumer of new messages

0 commit comments

Comments
 (0)