Producer-Consumer Pattern in spdlog Async Logging: Architecture and Implementation
spdlog implements the producer-consumer pattern for asynchronous logging by decoupling application threads that produce log_msg records via async_logger from a pool of worker threads that consume async_msg structures from a lock-protected circular buffer inside details::mpmc_blocking_queue.
The gabime/spdlog library uses a classic producer-consumer design to ensure that slow disk I/O never blocks fast application threads. In this architecture, an async_logger instance acts as the producer, packaging log events and handing them off to a global details::thread_pool. A fixed-size pool of worker threads then consumes those messages in the background, applying the configured sinks independently of the caller. Understanding this producer-consumer pattern in spdlog async logging is essential for tuning queue depth, overflow policies, and thread counts.
Core Components of the spdlog Producer-Consumer Pattern
Producer: async_logger and thread_pool::post_log
When the application calls logger->info(...) on an asynchronous logger, the async_logger::log method constructs a details::log_msg and forwards it to the global thread pool by invoking details::thread_pool::post_log. As implemented in src/details/thread_pool-inl.h (lines 57-62), this function wraps the payload in an async_msg that stores a shared pointer to the originating logger and the log content, then submits it to the queue.
Queue: mpmc_blocking_q
The shared channel is a details::mpmc_blocking_queue<async_msg> defined in src/details/mpmc_blocking_q.h. It is a lock-protected circular buffer that supports both blocking and non-blocking insertion. Producers use enqueue (lines 30-38) when the overflow policy is set to block, or enqueue_nowait (lines 40-47) when the policy is overrun_oldest, an option that drops the oldest entry to make room. The queue also tracks over-run and discard counters for diagnostics.
Consumer: Worker Threads and worker_loop_
The consumer side is handled by worker threads spawned during thread pool initialization via init_thread_pool or the default factory in include/spdlog/async.h (lines 40-48). Each thread runs worker_loop_, which repeatedly calls process_next_msg_ as shown in src/details/thread_pool-inl.h (lines 91-122). This consumer routine dequeues the next async_msg, inspects its msg_type, and dispatches it to backend_sink_it_ for log messages, backend_flush_ for flushes, or exits when a terminate message is received.
End-to-End Message Flow in spdlog Async Logging
The producer-consumer pipeline follows a strict lifecycle from logger creation to shutdown:
- Logger creation.
async_factory_impl::createininclude/spdlog/async.h(lines 31-55) builds anasync_loggerand ensures a globalthread_poolexists. - Logging call. The public logger API constructs a
log_msgand forwards it throughthread_pool::post_log. - Enqueue. The message is converted into an
async_msgand placed onto the MPMC queue by a producer thread. - Consume. Worker threads continuously dequeue messages inside
process_next_msg_and process them without blocking the original caller. - Shutdown. On thread pool destruction, a
terminatemessage is posted to each worker, causingprocess_next_msg_to returnfalseand the thread to join cleanly.
This design cleanly separates the fast producer path from the slow I/O-bound consumer path, ensuring that application threads only stall if the queue is full and the selected overflow policy is block.
Practical Code Examples
Creating an Asynchronous Logger with Default Blocking Policy
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
int main() {
// Initialise a global thread pool (optional – spdlog creates one automatically)
spdlog::init_thread_pool(8192, 2); // queue size, number of worker threads
// Create an async logger that writes to a file
auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
"async_file", "logs.txt");
logger->info("Hello from the producer thread!");
}
The call to create_async ultimately invokes async_factory_impl::create, which ensures a global thread pool exists and registers the new logger. Relevant source: include/spdlog/async.h, lines 31-55.
Using the Non-Blocking overrun_oldest Policy
auto nb_logger = spdlog::create_async_nb<spdlog::sinks::stdout_sink_mt>("nb_console");
nb_logger->warn("This uses the overrun-oldest policy.");
Here create_async_nb selects async_overflow_policy::overrun_oldest. The queue’s enqueue_nowait method is used, which discards the oldest entry when the queue is full. Relevant source: include/spdlog/async.h, lines 69-73 and src/details/mpmc_blocking_q.h, lines 40-47.
Flushing and Graceful Shutdown
logger->flush(); // posts a flush message to the queue
spdlog::shutdown(); // posts terminate messages, joins worker threads
Flushing posts an async_msg_type::flush to the queue (see post_flush in src/details/thread_pool-inl.h, lines 64-67). shutdown() ultimately destroys the thread pool, causing termination messages to be posted (see destructor, lines 45-49).
Key Source Files
include/spdlog/async.h— Public async API, thread-pool creation, and logger factories.include/spdlog/details/thread_pool.h— Declaration ofthread_pool, queue type, and public methods.src/details/thread_pool-inl.h— Implementation of the thread pool, worker loop, and message dispatch.src/details/mpmc_blocking_q.h— Multi-producer-multi-consumer blocking queue implementation.include/spdlog/async_logger.h— Async logger interface and thelogmethod that forwards to the pool.
Summary
- The producer-consumer pattern in spdlog async logging relies on
async_loggeras the producer,details::mpmc_blocking_queueas the bounded buffer, anddetails::thread_poolworker threads as the consumers. thread_pool::post_loginsrc/details/thread_pool-inl.h(lines 57-62) enqueuesasync_msgobjects, whileworker_loop_andprocess_next_msg_(lines 91-122) dequeue and dispatch them.- The MPMC queue supports both blocking (
enqueue) and non-blocking (enqueue_nowait) insertion strategies, selectable via overflow policy. - Shutdown is coordinated by posting
terminatemessages to each worker, guaranteeing clean thread joining without leaking log records.
Frequently Asked Questions
What is the role of async_logger in the spdlog producer-consumer pattern?
async_logger acts as the producer. When the application invokes a logging method, the logger builds a details::log_msg and immediately hands it off to the global details::thread_pool through post_log. This keeps the calling thread free from sink I/O.
How does spdlog handle a full queue in async logging?
Behavior is controlled by the async_overflow_policy. The default block policy causes producers to wait using the queue’s enqueue method. The overrun_oldest policy uses enqueue_nowait in src/details/mpmc_blocking_q.h (lines 40-47) to overwrite the oldest message when capacity is reached.
What happens during shutdown of the spdlog thread pool?
When the thread pool is destroyed or spdlog::shutdown() is called, the destructor posts an async_msg_type::terminate message to every worker thread in src/details/thread_pool-inl.h (lines 45-49). Each consumer thread then exits its worker_loop_ and joins cleanly.
Which source files implement the spdlog async producer-consumer queue?
The queue implementation lives in src/details/mpmc_blocking_q.h, while producer submission and consumer dispatch are implemented in src/details/thread_pool-inl.h. Public factory functions are declared in include/spdlog/async.h, and the logger interface is found in include/spdlog/async_logger.h.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →