Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions bench/logit_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ std::chrono::nanoseconds run_workload(
// Barrier to start together.
std::mutex start_mx;
std::condition_variable start_cv;
std::condition_variable ready_cv;
bool start_flag = false;
std::size_t ready = 0;

Expand All @@ -132,7 +133,7 @@ std::chrono::nanoseconds run_workload(
{
std::unique_lock<std::mutex> lk(start_mx);
++ready;
if (ready == scenario.producers) start_cv.notify_one();
if (ready == scenario.producers) ready_cv.notify_one();
start_cv.wait(lk, [&]{ return start_flag; });
}
for (std::size_t n = 0; n < per_thread[i]; ++n) {
Expand All @@ -150,7 +151,7 @@ std::chrono::nanoseconds run_workload(
std::chrono::steady_clock::time_point t0;
{
std::unique_lock<std::mutex> lk(start_mx);
start_cv.wait(lk, [&]{ return ready == scenario.producers; });
ready_cv.wait(lk, [&]{ return ready == scenario.producers; });
if (measure_duration) t0 = std::chrono::steady_clock::now();
start_flag = true;
start_cv.notify_all();
Expand Down
8 changes: 7 additions & 1 deletion docs/TaskExecutor.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,13 @@ const auto lost = LOGIT_GET_DROPPED_TASKS();
`set_queue_policy()`.
* The hot-resize barrier uses `m_resizing` and `m_resize_cv` so producers never
touch a ring buffer that is being rebuilt. This eliminates the data races that
TSAN previously reported on `try_pop()` vs. buffer assignment.
TSAN previously reported on `try_pop()` vs. buffer assignment. The barrier
only drops once the worker thread fully stops and the queue drains; if a sink
blocks the worker or `QueuePolicy::Block` keeps `m_active_tasks` above the
limit for more than one second, `set_max_queue_size()` abandons the hot
resize, clears `m_resizing`, and leaves the existing ring untouched so
producers cannot wait indefinitely. Non-MPSC builds perform the resize as an
atomic update of `m_max_queue_size`, so they are not subject to this stall.
* Non-MPSC builds rely solely on mutexes and had no known data races.
* The Emscripten path is single-threaded and should not be used concurrently.

Expand Down
35 changes: 27 additions & 8 deletions include/logit_cpp/logit/detail/TaskExecutor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
#include <deque>
#include <mutex>
#include <emscripten/emscripten.h>
#else
#include <thread>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <chrono>
#else
#include <thread>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <chrono>
#endif

// Enable lock-free MPSC ring integration (non-Emscripten) by defining:
Expand Down Expand Up @@ -323,8 +323,18 @@ namespace logit { namespace detail {
// Tell producers to pause before any wait()/stop conditions run.
m_resizing.store(true, std::memory_order_release);

// Drain the queue completely.
wait();
// Drain the queue completely, but do not wait forever if the worker
// is stalled (e.g., blocked sink or backpressure keeping
// m_active_tasks > 0). If we fail to drain before the deadline,
// abort the resize and re-open the barrier so producers can
// continue.
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(1);
if (!wait_until_idle_(deadline)) {
m_resizing.store(false, std::memory_order_release);
m_resize_cv.notify_all();
return;
}

// Stop the worker so it cannot touch m_mpsc_queue during the resize.
std::unique_lock<std::mutex> lk(m_queue_mutex);
Expand Down Expand Up @@ -474,6 +484,15 @@ namespace logit { namespace detail {
bool queue_empty_() const noexcept {
return m_mpsc_queue.empty();
}

bool wait_until_idle_(std::chrono::steady_clock::time_point deadline) {
std::unique_lock<std::mutex> lock(m_queue_mutex);
return m_queue_condition.wait_until(lock, deadline, [this]() {
return ((queue_empty_() &&
m_active_tasks.load(std::memory_order_relaxed) == 0) ||
m_stop_flag.load(std::memory_order_acquire));
});
}
#endif

TaskExecutor()
Expand Down