LogIt++
Header-only, macro-first C++ logging library
Loading...
Searching...
No Matches
TaskExecutor.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_TASKEXECUTOR_HPP_INCLUDED
3#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_TASKEXECUTOR_HPP_INCLUDED
4
8
9#include <functional>
10#include <atomic>
11#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
12 #include <deque>
13 #include <mutex>
14 #include <emscripten/emscripten.h>
15 #else
16 #include <thread>
17 #include <deque>
18 #include <mutex>
19 #include <condition_variable>
20 #include <chrono>
21#endif
22
23// Enable lock-free MPSC ring integration (non-Emscripten) by defining:
24// #define LOGIT_USE_MPSC_RING
25
26#if !defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__)
27 #ifdef LOGIT_USE_MPSC_RING
28 #include "MpscRingAny.hpp"
29 #endif
30#endif
31
32#include "QueuePolicy.hpp"
33
34namespace logit { namespace detail {
35
36# if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
37
43 class TaskExecutor {
44 public:
46 static TaskExecutor& get_instance() {
47 static TaskExecutor instance;
48 return instance;
49 }
50
54 void add_task(std::function<void()> task) {
55 if (!task) return;
56 bool schedule = false;
57 for (;;) {
58 schedule = false;
59 {
60 std::lock_guard<std::mutex> lk(m_mutex);
61 if (m_max_queue_size > 0 && m_tasks.size() >= m_max_queue_size) {
62 switch (m_overflow_policy) {
63 case QueuePolicy::DropNewest:
65 return;
66 case QueuePolicy::DropOldest:
67 if (!m_tasks.empty()) {
68 m_tasks.pop_front();
70 }
71 break;
72 case QueuePolicy::Block:
73 break; // handled after unlocking
74 }
75 if (m_overflow_policy == QueuePolicy::Block && m_tasks.size() >= m_max_queue_size) {
76 // fall through to drain outside lock
77 } else {
78 m_tasks.emplace_back(std::move(task));
79 schedule = !m_scheduled;
80 m_scheduled = m_scheduled || schedule;
81 break;
82 }
83 } else {
84 m_tasks.emplace_back(std::move(task));
85 schedule = !m_scheduled;
86 m_scheduled = m_scheduled || schedule;
87 break;
88 }
89 }
90 drain();
91 }
92 if (schedule) {
93 emscripten_async_call(&TaskExecutor::drain_thunk, this, 0);
94 }
95 }
96
98 void wait() { drain(); }
100 void shutdown() { drain(); }
101
103 void set_max_queue_size(std::size_t size) {
104 std::lock_guard<std::mutex> lk(m_mutex);
105 m_max_queue_size = size;
106 }
107
109 void set_queue_policy(QueuePolicy policy) {
110 std::lock_guard<std::mutex> lk(m_mutex);
111 m_overflow_policy = policy;
112 }
113
115 std::size_t dropped_tasks() const noexcept {
116 return m_dropped_tasks.load(std::memory_order_relaxed);
117 }
119 void reset_dropped_tasks() noexcept {
120 m_dropped_tasks.store(0, std::memory_order_relaxed);
121 }
122
123 private:
125 : m_max_queue_size(0),
126 m_overflow_policy(QueuePolicy::Block),
128 m_scheduled(false) {}
129 ~TaskExecutor() = default;
130 TaskExecutor(const TaskExecutor&) = delete;
131 TaskExecutor& operator=(const TaskExecutor&) = delete;
132 TaskExecutor(TaskExecutor&&) = delete;
134
135 std::deque<std::function<void()>> m_tasks;
136 std::mutex m_mutex;
137 std::size_t m_max_queue_size;
139 std::atomic<std::size_t> m_dropped_tasks;
140 bool m_scheduled;
141
142 static void drain_thunk(void* arg) {
143 static_cast<TaskExecutor*>(arg)->drain();
144 }
145
146 void drain() {
147 for (;;) {
148 std::function<void()> task;
149 {
150 std::lock_guard<std::mutex> lk(m_mutex);
151 if (m_tasks.empty()) {
152 m_scheduled = false;
153 break;
154 }
155 task = std::move(m_tasks.front());
156 m_tasks.pop_front();
157 }
158 task();
159 }
160 }
161 };
162
163# else // !Emscripten or pthreads
164
172 public:
178 static TaskExecutor* instance = new TaskExecutor();
179 return *instance;
180 }
181
186 void add_task(std::function<void()> task) {
187 if (!task) return;
188# ifndef LOGIT_USE_MPSC_RING
189 std::unique_lock<std::mutex> lock(m_queue_mutex);
190 if (m_stop_flag.load(std::memory_order_acquire)) return;
191 if (m_max_queue_size > 0 && m_tasks_queue.size() >= m_max_queue_size) {
192 switch (m_overflow_policy.load(std::memory_order_relaxed)) {
193 case QueuePolicy::DropNewest:
195 return;
196 case QueuePolicy::DropOldest:
197 if (!m_tasks_queue.empty()) {
198 m_tasks_queue.pop_front();
200 }
201 break;
202 case QueuePolicy::Block:
203 m_queue_condition.wait(lock, [this]() {
204 return m_tasks_queue.size() < m_max_queue_size ||
205 m_stop_flag.load(std::memory_order_acquire);
206 });
207 if (m_stop_flag.load(std::memory_order_acquire)) return;
208 break;
209 }
210 }
211 m_tasks_queue.push_back(std::move(task));
212 lock.unlock();
213 m_queue_condition.notify_one();
214# else
215 enter_producer_();
216
217 std::function<void()> local_task = std::move(task);
218 bool done = false;
219
220 while (!done) {
221 if (m_stop_flag.load(std::memory_order_acquire)) {
222 break;
223 }
224
225 const auto policy = m_overflow_policy.load(std::memory_order_relaxed);
226
227 // Backpressure relies on the count of in-flight tasks.
228 if (policy == QueuePolicy::Block &&
229 m_max_queue_size > 0 &&
230 m_active_tasks.load(std::memory_order_relaxed) >= m_max_queue_size)
231 {
232 std::unique_lock<std::mutex> lk(m_cv_mutex);
233 m_cv.wait_for(lk, std::chrono::microseconds(LOGIT_TASK_EXECUTOR_BLOCK_WAIT_USEC));
234 continue;
235 }
236
237 // Try to push into the ring buffer.
238 // Move into the ring: std::function's move construction is
239 // noexcept, while copying may allocate and throw. The ring
240 // deliberately accepts only no-throw construction so a
241 // producer can never leave a claimed cell unpublished.
242 if (m_mpsc_queue.try_push(std::move(local_task))) {
243 m_cv.notify_one(); // wake the worker
244 break;
245 }
246
247 // Apply the configured overflow policy when the ring is full.
248 switch (policy) {
249 case QueuePolicy::DropNewest:
250 m_dropped_tasks.fetch_add(1, std::memory_order_relaxed);
251 done = true;
252 break;
253
254 case QueuePolicy::DropOldest:
255 // Safe MPSC behaviour: drop the incoming task.
256 // Preserves ordering and avoids producer/consumer deadlocks.
257 m_dropped_tasks.fetch_add(1, std::memory_order_relaxed);
258 done = true;
259 break;
260
261 case QueuePolicy::Block: {
262 std::unique_lock<std::mutex> lk(m_cv_mutex);
263 m_cv.wait_for(lk, std::chrono::microseconds(LOGIT_TASK_EXECUTOR_BLOCK_WAIT_USEC));
264 break;
265 }
266 }
267 }
268 leave_producer_();
269# endif
270 }
271
273 void wait() {
274# ifndef LOGIT_USE_MPSC_RING
275 std::unique_lock<std::mutex> lock(m_queue_mutex);
276 m_queue_condition.wait(lock, [this]() {
277 return ((m_tasks_queue.empty() &&
278 m_active_tasks.load(std::memory_order_relaxed) == 0) ||
279 m_stop_flag.load(std::memory_order_acquire));
280 });
281# else
282 std::unique_lock<std::mutex> lock(m_queue_mutex);
283 m_queue_condition.wait(lock, [this]() {
284 return ((queue_empty_() &&
285 m_active_tasks.load(std::memory_order_relaxed) == 0) ||
286 m_stop_flag.load(std::memory_order_acquire));
287 });
288# endif
289 }
290
292 void shutdown() {
293 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
294# ifndef LOGIT_USE_MPSC_RING
295 std::unique_lock<std::mutex> lock(m_queue_mutex);
296 m_stop_flag.store(true, std::memory_order_release);
297 lock.unlock();
298 m_queue_condition.notify_all();
299 if (m_worker_thread.joinable()) {
300 m_worker_thread.join();
301 }
302# else
303 {
304 std::lock_guard<std::mutex> lock(m_queue_mutex);
305 m_stop_flag.store(true, std::memory_order_release);
306 }
307 m_cv.notify_all();
308 m_queue_condition.notify_all();
309 if (m_worker_thread.joinable()) {
310 m_worker_thread.join();
311 }
312# endif
313 }
314
318 void set_max_queue_size(std::size_t size) {
319 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
320 if (m_stop_flag.load(std::memory_order_acquire)) return;
321# ifdef LOGIT_USE_MPSC_RING
322 // Tell producers to pause before any wait()/stop conditions run.
323 m_resizing.store(true, std::memory_order_release);
324 const auto deadline = std::chrono::steady_clock::now() +
325 std::chrono::seconds(1);
326 // Existing producers may be blocked by backpressure, so do not
327 // wait forever for the resize barrier.
328 if (!wait_until_producers_paused_(deadline)) {
329 m_resizing.store(false, std::memory_order_release);
330 m_resize_cv.notify_all();
331 return;
332 }
333
334 // Drain the queue completely, but do not wait forever if the worker
335 // is stalled (e.g., blocked sink or backpressure keeping
336 // m_active_tasks > 0). If we fail to drain before the deadline,
337 // abort the resize and re-open the barrier so producers can
338 // continue.
339 if (!wait_until_idle_(deadline)) {
340 m_resizing.store(false, std::memory_order_release);
341 m_resize_cv.notify_all();
342 return;
343 }
344
345 // Stop the worker so it cannot touch m_mpsc_queue during the resize.
346 std::unique_lock<std::mutex> lk(m_queue_mutex);
347 m_stop_flag.store(true, std::memory_order_relaxed);
348 lk.unlock();
349
350 m_cv.notify_all();
351 m_queue_condition.notify_all();
352 if (m_worker_thread.joinable()) {
353 m_worker_thread.join();
354 }
355
356 // Reinitialise the parameters and the ring on a single thread.
357 lk.lock();
358 m_max_queue_size = size;
359 const std::size_t cap =
360 (m_max_queue_size == 0 ? m_default_ring_cap : m_max_queue_size);
361 m_mpsc_queue = MpscRingAny<std::function<void()>>(cap);
362 // Reset counters (except drops) because the queue is empty.
363 m_active_tasks.store(0, std::memory_order_relaxed);
364 // Keep m_dropped_tasks untouched; tests manage it via macros.
365 lk.unlock();
366
367 // Clear the stop flag and restart the worker thread.
368 m_stop_flag.store(false, std::memory_order_relaxed);
370
371 // Re-open the barrier so producers can continue.
372 m_resizing.store(false, std::memory_order_release);
373 m_resize_cv.notify_all();
374# else
375 std::lock_guard<std::mutex> lock(m_queue_mutex);
376 m_max_queue_size = size;
377# endif
378 }
379
382 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
383 if (m_stop_flag.load(std::memory_order_acquire)) return;
384 std::lock_guard<std::mutex> lock(m_queue_mutex);
385 m_overflow_policy.store(policy, std::memory_order_relaxed);
386 }
387
389 std::size_t dropped_tasks() const noexcept {
390 return m_dropped_tasks.load(std::memory_order_relaxed);
391 }
393 void reset_dropped_tasks() noexcept {
394 m_dropped_tasks.store(0, std::memory_order_relaxed);
395 }
396
397 private:
398 mutable std::mutex m_lifecycle_mutex;
399 #ifndef LOGIT_USE_MPSC_RING
400 std::deque<std::function<void()>> m_tasks_queue;
401 mutable std::mutex m_queue_mutex;
402 std::condition_variable m_queue_condition;
403 std::thread m_worker_thread;
404 std::atomic<bool> m_stop_flag;
405 std::size_t m_max_queue_size;
406 std::atomic<QueuePolicy> m_overflow_policy;
407 std::atomic<std::size_t> m_dropped_tasks;
408 std::atomic<std::size_t> m_active_tasks;
409 #else
410 mutable std::mutex m_queue_mutex;
411 std::condition_variable m_queue_condition;
412
413 std::condition_variable m_cv;
414 std::mutex m_cv_mutex;
415
416 std::atomic<bool> m_resizing;
417 std::condition_variable m_resize_cv;
418 std::atomic<std::size_t> m_active_producers;
419
420 std::thread m_worker_thread;
421 std::atomic<bool> m_stop_flag;
422 std::size_t m_max_queue_size;
423 std::atomic<QueuePolicy> m_overflow_policy;
424 std::atomic<std::size_t> m_dropped_tasks;
425 std::atomic<std::size_t> m_active_tasks;
426
427 const std::size_t m_default_ring_cap = LOGIT_TASK_EXECUTOR_DEFAULT_RING_CAPACITY;
428 MpscRingAny<std::function<void()>> m_mpsc_queue;
429 #endif
430
432 #ifndef LOGIT_USE_MPSC_RING
433 for (;;) {
434 std::function<void()> task;
435 std::unique_lock<std::mutex> lock(m_queue_mutex);
436 m_queue_condition.wait(lock, [this]() {
437 return !m_tasks_queue.empty() || m_stop_flag.load(std::memory_order_acquire);
438 });
439 if (m_stop_flag.load(std::memory_order_acquire) && m_tasks_queue.empty()) {
440 break;
441 }
442 task = std::move(m_tasks_queue.front());
443 m_tasks_queue.pop_front();
444 m_active_tasks.fetch_add(1, std::memory_order_relaxed);
445 lock.unlock();
446 m_queue_condition.notify_one();
447
448 task();
449
450 lock.lock();
451 m_active_tasks.fetch_sub(1, std::memory_order_relaxed);
452 if (m_tasks_queue.empty() && m_active_tasks.load(std::memory_order_relaxed) == 0) {
453 m_queue_condition.notify_all();
454 }
455 lock.unlock();
456 }
457 #else
458 for (;;) {
459 bool drained_any = false;
460 std::function<void()> task;
461
463 while (budget--) {
464 // Count the pop attempt as active so wait() cannot observe
465 // an empty ring between try_pop() freeing a cell and the
466 // dequeued task starting execution.
467 m_active_tasks.fetch_add(1, std::memory_order_relaxed);
468 if (!m_mpsc_queue.try_pop(task)) {
469 m_active_tasks.fetch_sub(1, std::memory_order_relaxed);
470 break;
471 }
472
473 drained_any = true;
474
475 task();
476
477 m_active_tasks.fetch_sub(1, std::memory_order_relaxed);
478 m_cv.notify_one(); // freed an in-flight slot
479 }
480
481 if (queue_empty_() && m_active_tasks.load(std::memory_order_relaxed) == 0) {
482 std::unique_lock<std::mutex> lock(m_queue_mutex);
483 m_queue_condition.notify_all(); // notify wait()
484 m_cv.notify_all(); // wake producers blocked on Block
485 if (m_stop_flag.load(std::memory_order_acquire)) {
486 break;
487 }
488 }
489
490 if (!drained_any) {
491 std::unique_lock<std::mutex> lk(m_cv_mutex);
492 if (m_stop_flag.load(std::memory_order_acquire) && queue_empty_()) {
493 break;
494 }
495 m_cv.wait_for(lk, std::chrono::milliseconds(1));
496 }
497 }
498 #endif
499 }
500
501 #ifdef LOGIT_USE_MPSC_RING
502 bool queue_empty_() const noexcept {
503 return m_mpsc_queue.empty();
504 }
505
506 bool wait_until_idle_(std::chrono::steady_clock::time_point deadline) {
507 std::unique_lock<std::mutex> lock(m_queue_mutex);
508 return m_queue_condition.wait_until(lock, deadline, [this]() {
509 return ((queue_empty_() &&
510 m_active_tasks.load(std::memory_order_relaxed) == 0) ||
511 m_stop_flag.load(std::memory_order_acquire));
512 });
513 }
514
515 void enter_producer_() {
516 for (;;) {
517 if (m_resizing.load(std::memory_order_acquire)) {
518 std::unique_lock<std::mutex> lk(m_cv_mutex);
519 m_resize_cv.wait(lk, [this]() {
520 return !m_resizing.load(std::memory_order_acquire);
521 });
522 continue;
523 }
524
525 m_active_producers.fetch_add(1, std::memory_order_acq_rel);
526 if (!m_resizing.load(std::memory_order_acquire)) {
527 return;
528 }
529 leave_producer_();
530 }
531 }
532
533 void leave_producer_() {
534 if (m_active_producers.fetch_sub(1, std::memory_order_acq_rel) == 1 &&
535 m_resizing.load(std::memory_order_acquire)) {
536 m_resize_cv.notify_all();
537 }
538 }
539
540 bool wait_until_producers_paused_(std::chrono::steady_clock::time_point deadline) {
541 std::unique_lock<std::mutex> lk(m_cv_mutex);
542 return m_resize_cv.wait_until(lk, deadline, [this]() {
543 return m_active_producers.load(std::memory_order_acquire) == 0;
544 });
545 }
546 #endif
547
549 #ifndef LOGIT_USE_MPSC_RING
550 : m_stop_flag(false),
552 m_overflow_policy(QueuePolicy::Block),
555 #else
556 : m_resizing(false),
557 m_active_producers(0),
559 m_stop_flag(false),
561 m_overflow_policy(QueuePolicy::Block),
564 m_mpsc_queue(m_default_ring_cap)
565 #endif
566 {
568 }
569
571 shutdown();
572 }
573
574 TaskExecutor(const TaskExecutor&) = delete;
578 };
579
580#endif // Emscripten split
581
582}} // namespace logit::detail
583
584#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_DETAIL_TASKEXECUTOR_HPP_INCLUDED
Bounded MPSC ring buffer with arbitrary capacity (C++11).
Thread-safe task executor backed by a dedicated worker thread.
std::atomic< std::size_t > m_dropped_tasks
void set_max_queue_size(std::size_t size)
Update the maximum queue size (0 disables the limit).
std::deque< std::function< void()> > m_tasks_queue
TaskExecutor(const TaskExecutor &)=delete
std::atomic< bool > m_stop_flag
TaskExecutor & operator=(const TaskExecutor &)=delete
std::condition_variable m_queue_condition
void reset_dropped_tasks() noexcept
Reset the overflow counter to zero.
TaskExecutor & operator=(TaskExecutor &&)=delete
std::atomic< QueuePolicy > m_overflow_policy
void shutdown()
Stop the worker thread and drain outstanding tasks.
std::atomic< std::size_t > m_active_tasks
TaskExecutor(TaskExecutor &&)=delete
void set_queue_policy(QueuePolicy policy)
Change the overflow policy for newly submitted tasks.
std::size_t dropped_tasks() const noexcept
Return the number of tasks dropped by the overflow policy.
void add_task(std::function< void()> task)
Enqueue a task for asynchronous execution.
std::mutex m_lifecycle_mutex
Serializes shutdown with lifecycle-changing operations.
void wait()
Block until the queue is empty or a shutdown is requested.
static TaskExecutor & get_instance()
Returns the global executor instance.
#define LOGIT_TASK_EXECUTOR_DRAIN_BUDGET
Maximum number of tasks drained per worker iteration in ring-buffer builds.
Definition config.hpp:219
#define LOGIT_TASK_EXECUTOR_DEFAULT_RING_CAPACITY
Default capacity for the task executor ring buffer when unlimited is requested.
Definition config.hpp:224
#define LOGIT_TASK_EXECUTOR_BLOCK_WAIT_USEC
Defines the sleep duration (in microseconds) used by TaskExecutor when blocking producers.
Definition config.hpp:31
QueuePolicy
Queue overflow handling policy used by TaskExecutor and SingleThreadExecutor.
The primary namespace for the LogIt++ library.