Dont spin main for queue (#443)

* Use DynamicLatch in DispatchQueue to avoid main thread spinning

Replace the busy-yielding pending_task_count_ loop in DispatchQueue::finishTasks
with a blocking DynamicLatch. This avoids having the main thread consume CPU
cycles while waiting for dispatched tasks to complete.

The DynamicLatch implementation uses C++20 std::atomic::wait/notify_all for
efficient blocking and wakeup, with proper release-acquire semantics to ensure
task results are visible to the waiting thread.

* Reformat DynamicLatch to match DispatchQueue style

* Update attribution headers in DispatchQueue files to note modifications
This commit is contained in:
Drew Lewis 2026-06-06 19:45:46 -04:00 committed by GitHub
parent ffe126af2a
commit b3458485ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 14 deletions

View File

@ -1,11 +1,14 @@
// Author Phillip Johnston
// Original Author: Phillip Johnston
// Licensed under CC0 1.0 Universal
// https://github.com/embeddedartistry/embedded-resources/blob/master/examples/cpp/dispatch.cpp
// https://embeddedartistry.com/blog/2017/2/1/dispatch-queues?rq=dispatch
// Original source: https://github.com/embeddedartistry/embedded-resources/blob/master/examples/cpp/dispatch.cpp
// Original article: https://embeddedartistry.com/blog/2017/2/1/dispatch-queues?rq=dispatch
//
// Modified for OpenSTA to use C++20 non-spinning DynamicLatch for synchronization.
#pragma once
#include <atomic>
#include <cstddef>
#include <condition_variable>
#include <cstdint>
#include <functional>
@ -16,6 +19,49 @@
namespace sta {
class DynamicLatch
{
public:
explicit DynamicLatch(std::ptrdiff_t initial_count = 0) :
count_(initial_count)
{
}
// Delete copy/move constructors to prevent accidental slicing/copying of atomics
DynamicLatch(const DynamicLatch&) = delete;
DynamicLatch& operator=(const DynamicLatch&) = delete;
// Increases the latch count (used when a new task is dispatched)
void
countUp()
{
count_.fetch_add(1, std::memory_order_release);
}
// Decreases the latch count and wakes waiting threads if it hits zero
void
countDown(std::ptrdiff_t n = 1)
{
if (count_.fetch_sub(n, std::memory_order_release) == n) {
count_.notify_all();
}
}
// Blocks until the count reaches zero
void
wait() const
{
std::ptrdiff_t current = count_.load(std::memory_order_acquire);
while (current != 0) {
count_.wait(current, std::memory_order_acquire);
current = count_.load(std::memory_order_acquire);
}
}
private:
mutable std::atomic<std::ptrdiff_t> count_{0};
};
class DispatchQueue
{
using fp_t = std::function<void(int thread)>;
@ -45,7 +91,7 @@ private:
std::vector<std::thread> threads_;
std::queue<fp_t> q_;
std::condition_variable cv_;
std::atomic<size_t> pending_task_count_;
DynamicLatch pending_task_count_latch_;
bool quit_ = false;
};

View File

@ -1,15 +1,16 @@
// Author Phillip Johnston
// Original Author: Phillip Johnston
// Licensed under CC0 1.0 Universal
// https://github.com/embeddedartistry/embedded-resources/blob/master/examples/cpp/dispatch.cpp
// https://embeddedartistry.com/blog/2017/2/1/dispatch-queues?rq=dispatch
// Original source: https://github.com/embeddedartistry/embedded-resources/blob/master/examples/cpp/dispatch.cpp
// Original article: https://embeddedartistry.com/blog/2017/2/1/dispatch-queues?rq=dispatch
//
// Modified for OpenSTA to use C++20 non-spinning DynamicLatch for synchronization.
#include "DispatchQueue.hh"
namespace sta {
DispatchQueue::DispatchQueue(size_t thread_count) :
threads_(thread_count),
pending_task_count_(0)
threads_(thread_count)
{
for(size_t i = 0; i < thread_count; i++)
threads_[i] = std::thread(&DispatchQueue::dispatch_thread_handler, this, i);
@ -58,8 +59,7 @@ DispatchQueue::getThreadCount() const
void
DispatchQueue::finishTasks()
{
while (pending_task_count_.load(std::memory_order_acquire) != 0)
std::this_thread::yield();
pending_task_count_latch_.wait();
}
void
@ -67,7 +67,7 @@ DispatchQueue::dispatch(const fp_t& op)
{
std::unique_lock<std::mutex> lock(lock_);
q_.push(op);
pending_task_count_++;
pending_task_count_latch_.countUp();
// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
@ -80,7 +80,7 @@ DispatchQueue::dispatch(fp_t&& op)
{
std::unique_lock<std::mutex> lock(lock_);
q_.push(std::move(op));
pending_task_count_++;
pending_task_count_latch_.countUp();
// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
@ -106,7 +106,7 @@ DispatchQueue::dispatch_thread_handler(size_t i)
op(i);
pending_task_count_--;
pending_task_count_latch_.countDown();
lock.lock();
}
} while (!quit_);