![]() |
LogIt++
Header-only, macro-first C++ logging library
|
The global TaskExecutor powers backends that use LogIt++'s shared asynchronous executor. Some backends are synchronous, dedicated-executor configurations use per-backend SingleThreadExecutor instances, and OTLP backends maintain their own exporter queues. This document describes how the shared executor behaves across build configurations, provides guidance on tuning backpressure policies, and explains the lifetime guarantees that logger integrations rely on.
LOGIT_USE_MPSC_RING)m_worker_thread) consumes a std::deque protected by m_queue_mutex.m_queue_condition and the m_stop_flag atomic.LOGIT_USE_MPSC_RING)m_mpsc_queue, a lock-free MpscRingAny<std::function<void()>> with a single consumer thread.m_cv + m_cv_mutex coordinate sleepers for both the worker and producers that wait for capacity during QueuePolicy::Block.m_queue_condition wakes wait() callers once the queue drains.m_active_tasks tracks in-flight work so that Block limits concurrent execution and wait() can determine quiescence.m_stop_flag terminates the worker and stops accepting new tasks.std::deque guarded by m_mutex.emscripten_async_call scheduled from the main loop.QueuePolicy controls what happens when the queue reaches max_queue_size (0 means "unbounded").
Blockm_active_tasks to count in-flight work. If the counter reaches the limit, producers wait. The non-MPSC build waits on m_queue_condition. The MPSC build parks on m_cv with short sleeps while the worker drains tasks. The sleep interval defaults to LOGIT_TASK_EXECUTOR_BLOCK_WAIT_USEC microseconds (200 by default) and can be overridden at compile time. This policy avoids loss at the expense of producer-side backpressure.DropNewestm_dropped_tasks is incremented.DropOldestm_dropped_tasks still counts these rejections.The drop counter is observable via TaskExecutor::dropped_tasks() and exposed to end users through LOGIT_GET_DROPPED_TASKS().
LOGIT_USE_MPSC_RING)set_max_queue_size() performs a "hot" resize without tearing down the application.
m_resizing is set to true with release semantics.add_task() are allowed to finish or the resize is abandoned after the bounded resize deadline.wait() drains the queue and ensures m_active_tasks == 0.m_stop_flag, notifying sleepers, and joining the thread so it no longer touches m_mpsc_queue.m_dropped_tasks intact but resets m_active_tasks to 0 because the queue is empty.m_resizing flips back to false and m_resize_cv.notify_all() wakes producers that parked at the start of add_task().While the resize is in progress, new producers briefly wait on m_resize_cv. No accepted tasks are lost, and the consumer thread never observes partially initialised ring buffers. Calling set_max_queue_size() or set_queue_policy() after shutdown is a no-op; these calls must not restart or mutate the stopped singleton worker.
DropNewest and DropOldest both drop the incoming task; accepted tasks keep their order.wait() returns once the queue is empty and m_active_tasks == 0, or when a shutdown is requested. In MPSC builds the worker marks a pop attempt active before removing a task, so wait() cannot return in the narrow window between a dequeued cell becoming free and the task body starting.shutdown() blocks until the worker thread terminates. It is safe to call multiple times.TaskExecutor::get_instance() intentionally stores the singleton inside a static TaskExecutor* instance = new TaskExecutor();. This lets the executor outlive static destructors inside logger components. Applications may call shutdown() explicitly (for example during test teardown), but the singleton remains valid until the process terminates.
Logger backends with Config::use_dedicated_executor=true own a SingleThreadExecutor instead of using this singleton. Native builds create one worker thread per configured logger, while single-threaded Emscripten builds use a cooperative per-instance queue. Logger::shutdown() calls each backend's ILogger::shutdown() hook before stopping the global executor so these logger-owned workers drain and stop cleanly. For SingleThreadExecutor, wait() waits for already accepted tasks to drain, while shutdown() rejects new tasks and joins the worker on native builds. Full lifecycle guarantees (including no-op behaviour after shutdown) are documented in the Doxygen comments of SingleThreadExecutor.hpp.
When targeting Emscripten without pthread support:
Block is approximated by invoking drain() from the producer path until the deque has room. DropNewest/DropOldest mirror the deque operations exactly.emscripten_async_call, which schedules a drain on the browser event loop. This keeps logging compatible with the cooperative execution model used in WebAssembly UI scenarios.Public methods exposed by TaskExecutor:
set_max_queue_size(std::size_t size) — change the queue capacity (0 disables the limit). Trigger a hot resize on MPSC builds.set_queue_policy(QueuePolicy policy) — change overflow behaviour.add_task(std::function<void()> fn) — enqueue work for the background worker.wait() — block until the queue drains or stop is requested.shutdown() — stop the worker thread and release resources.dropped_tasks() and reset_dropped_tasks() — inspect or reset the overflow counter.Macros from the supported <logit.hpp> entry point map directly onto these calls:
LOGIT_SET_MAX_QUEUE(size) → set_max_queue_size(size)LOGIT_SET_QUEUE_POLICY(mode) → set_queue_policy(mode)LOGIT_QUEUE_BLOCK, LOGIT_QUEUE_DROP_NEWEST, LOGIT_QUEUE_DROP_OLDEST select the enum value.LOGIT_GET_DROPPED_TASKS() and LOGIT_RESET_DROPPED_TASKS() forward to the counter helpers.Basic setup using macros:
Hot resize while the system is running (only with LOGIT_USE_MPSC_RING):
Inspecting drops under DropNewest:
add_task() concurrently with set_max_queue_size() and set_queue_policy().m_resizing, m_resize_cv, and an active-producer counter 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. The barrier only proceeds once producers have paused, the worker thread fully stops, and the queue drains; if a sink blocks the worker or QueuePolicy::Block prevents producers from reaching the pause point 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.QueuePolicy::Block limits the number of in-flight tasks tracked by m_active_tasks. Use it to introduce producer-side backpressure when the downstream sinks are expensive.LOGIT_TASK_EXECUTOR_DRAIN_BUDGET tasks per iteration when the ring is enabled. Increase this "budget" in TaskExecutor::worker_function() if your workload generates extremely large bursts and the worker sleeps too often. Reducing it can lower per-iteration latency for latency-sensitive applications.LOGIT_TASK_EXECUTOR_DEFAULT_RING_CAPACITY at compile time to select a different default capacity when LOGIT_USE_MPSC_RING is active.dropped_tasks() during load testing to verify that the chosen policy matches the application's tolerance for loss.