Thread Safety Considerations for Multi-Threaded spdlog Loggers: A Complete Guide
spdlog guarantees thread-safe logging for concurrent applications through atomic operations in the spdlog::logger class and lock-free queues for async loggers, though the set_error_handler() method remains the sole exception requiring external synchronization.
The gabime/spdlog library is engineered for high-performance multi-threaded C++ applications, providing comprehensive thread safety mechanisms that vary between synchronous and asynchronous logging modes. Understanding these thread safety considerations ensures robust logging when multiple threads share logger instances across your application.
Core Thread Safety Mechanisms
spdlog implements thread safety through distinct architectural patterns depending on whether you use synchronous or asynchronous loggers.
Synchronous Logger Protection
The core spdlog::logger class defined in include/spdlog/logger.h (lines 6-12) provides thread-safe operations for all public API methods except set_error_handler(). The implementation relies on two key mechanisms:
should_log()reads an atomiclevel_variable to check logging thresholds without lockinglog_it_()acquires mutex locks only when necessary, with sinks maintaining independent formatter instances
As noted in include/spdlog/logger.h (lines 14-15), each sink holds its own formatter, ensuring concurrent calls to the same logger do not share formatter state. This eliminates data races when formatting messages from different threads.
Asynchronous Logging Architecture
For high-throughput scenarios, spdlog::async_logger uses a lock-free architecture. The implementation in include/spdlog/async.h queues messages into a ring buffer defined in include/spdlog/details/mpmc_blocking_q.h, which supports multiple producers safely.
The background processing relies on include/spdlog/details/thread_pool.h, where the post method offers lock-free enqueue operations for producer threads. Worker threads execute a simple while (queue.pop(...)) loop to process queued messages.
Global Registry Safety
The global logger registry implemented in include/spdlog/details/registry.h (lines 9-10) is fully thread-safe, allowing loggers to be registered, retrieved, or removed from any thread without external synchronization.
The Critical Exception: Error Handlers
While spdlog provides comprehensive thread safety, set_error_handler() is the only non-thread-safe method in the spdlog::logger class. You must configure error handlers before spawning any threads that might invoke the logger. Changing error handlers after threads begin logging creates a race condition on the handler function pointer.
Compile-Time Thread Safety Configuration
spdlog provides macros in include/spdlog/spdlog.h (lines 84-85) to customize thread safety at compile time:
SPDLOG_NO_THREAD_SAFETY: Removes internal mutexes from all loggers, making them non-thread-safe but maximizing performance for single-threaded applicationsSPDLOG_DISABLE_DEFAULT_LOGGER: Disables the thread-safe default logger, leaving only a non-thread-safe "_st" variant
Only disable these protections when you can guarantee single-threaded access to logger instances.
Practical Implementation Guidelines
Follow these guidelines when deploying spdlog in multi-threaded environments:
-
Choose the appropriate logger type for your concurrency needs. Use
spdlog::default_logger()or standardspdlog::loggerinstances for multi-threaded access, or create async loggers usingspdlog::async_factorywhen logging from many threads simultaneously. -
Initialize error handlers early. Call
set_error_handler()during application startup before any worker threads begin logging operations. -
Understand sink-level isolation. While each sink formats its own copy of messages independently, remember that file I/O sinks rely on OS-level synchronization. Two threads logging identical messages will produce two independently formatted strings.
-
Size async queues appropriately. When creating async loggers with
spdlog::init_thread_pool(queue_size, thread_count), ensure the ring buffer accommodates burst traffic. If the queue fills, producer threads block until space becomes available. -
Consider performance trade-offs. Async mode eliminates per-call mutex locking but introduces memory ordering costs from the lock-free queue. For extremely low-latency single-threaded logging, compile with
SPDLOG_NO_THREAD_SAFETYand use the "_st" variants.
Multi-Threaded Logging Examples
Synchronous Thread-Safe Logging
The default logger accessed via spdlog::default_logger() is thread-safe as implemented in include/spdlog/spdlog.h (lines 129-130):
#include <spdlog/spdlog.h>
#include <thread>
void worker(int id) {
auto logger = spdlog::default_logger(); // thread-safe logger
logger->info("Worker {} started", id);
// ... do work ...
logger->info("Worker {} finished", id);
}
int main() {
spdlog::set_level(spdlog::level::info); // global log level
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); t2.join();
}
Asynchronous Logging with Thread Pool
Create lock-free multi-threaded logging using the async factory:
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <thread>
int main() {
// Create a thread-pool with 2 worker threads and a queue of 8192 messages
spdlog::init_thread_pool(8192, 2);
// Create an async logger that writes to a file
auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>(
"async_file", "logs/async.log");
async_file->info("Async logger started");
std::thread t1([&](){ async_file->info("Message from thread 1"); });
std::thread t2([&](){ async_file->info("Message from thread 2"); });
t1.join(); t2.join();
async_file->flush(); // optional – forces queue drain
}
Disabling Thread Safety for Single-Threaded Optimization
Compile with -DSPDLOG_NO_THREAD_SAFETY to remove synchronization overhead:
// Compile with -DSPDLOG_NO_THREAD_SAFETY
#include <spdlog/spdlog.h>
int main() {
// No internal mutex – fastest possible logging, but NOT safe across threads
spdlog::info("Single-threaded fast logging");
}
Summary
- Default thread safety: The
spdlog::loggerclass protects all operations exceptset_error_handler()using atomic operations and mutexes, as defined ininclude/spdlog/logger.h. - Sink isolation: Each sink maintains independent formatters, preventing shared state corruption during concurrent formatting operations.
- Async architecture:
spdlog::async_loggeruses lock-free queues ininclude/spdlog/details/mpmc_blocking_q.hand background thread pools for high-throughput logging. - Registry protection: The global registry in
include/spdlog/details/registry.hensures thread-safe logger registration and retrieval. - Compile-time options:
SPDLOG_NO_THREAD_SAFETYandSPDLOG_DISABLE_DEFAULT_LOGGERallow single-threaded optimizations when concurrent access is not required.
Frequently Asked Questions
Is spdlog thread-safe by default?
Yes, according to the gabime/spdlog source code, the default spdlog::logger and spdlog::default_logger() are thread-safe for all operations except set_error_handler(). The implementation uses atomic operations for log level checks and mutex protection for sink operations, ensuring safe concurrent access from multiple threads without external synchronization.
What is the difference between synchronous and asynchronous thread safety in spdlog?
Synchronous loggers use mutex locks within the log_it_() method to protect sink writes, while asynchronous loggers eliminate per-call locking by delegating messages to a lock-free ring buffer processed by a background thread pool. The async approach in include/spdlog/async.h uses the lock-free queue defined in include/spdlog/details/mpmc_blocking_q.h, making it more scalable for high-contention scenarios with many producer threads.
Can I disable thread safety to improve logging performance?
Yes, by defining SPDLOG_NO_THREAD_SAFETY before including spdlog headers or compiling with -DSPDLOG_NO_THREAD_SAFETY, you remove all internal mutexes from the logger classes. This compilation flag, referenced in include/spdlog/spdlog.h (lines 84-85), creates non-thread-safe logger variants that offer maximum performance for single-threaded applications but will cause data races if accessed concurrently.
Is it safe to share a single logger instance across multiple threads?
Absolutely. The spdlog::logger class is designed for shared use across threads, with each sink maintaining its own formatter instance to prevent cross-thread interference. However, you must call set_error_handler() before threads start, as this method is the only non-thread-safe operation in the logger API according to the source in include/spdlog/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 →