Multi-threaded Logger Implementation
Develop a high-performance, thread-safe logging system that can handle concurrent write requests from multiple threads. This project introduces you to concurrent programming concepts in C++.
Project Structure
The logger is organized as follows:
phase1/logger/
├── CMakeLists.txt
├── include/
│ └── logger.h # Logger interface and classes
└── src/
└── logger.cpp # Implementation of logging functionality
Learning Objectives
During this project, you will:
- Implement thread-safe data structures using mutexes and condition variables
- Understand producer-consumer patterns
- Explore lock-free programming concepts (optional advanced feature)
- Design asynchronous systems
- Apply RAII (Resource Acquisition Is Initialization) principles
- Practice proper C++ concurrency patterns
Core Concepts
Thread Safety Requirements
Your logger must safely handle concurrent access from multiple threads:
- Multiple threads can log messages simultaneously
- Log messages must not be corrupted or interleaved
- Performance should not degrade significantly with many threads
- Critical sections should be minimized
Asynchronous Logging Pattern
To minimize logging overhead on application threads, implement an asynchronous design:
- Application threads write log entries to a queue
- A dedicated logger thread processes the queue
- The logger thread writes entries to the output destination
Implementation Architecture
Logging Interface
Provide a simple, easy-to-use interface:
enum class LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
};
class Logger {
public:
static void log(LogLevel level, const std::string& message);
static void set_output_file(const std::string& filepath);
static void set_level(LogLevel level);
// Convenience macros/functions
static void debug(const std::string& msg);
static void info(const std::string& msg);
static void warn(const std::string& msg);
static void error(const std::string& msg);
};
Queue-Based Architecture
Implement a thread-safe queue for message buffering:
template<typename T>
class ThreadSafeQueue {
private:
mutable std::mutex mtx_;
std::queue<T> queue_;
std::condition_variable cv_;
public:
void push(T item);
T pop(); // Blocks until item is available
bool try_pop(T& item); // Non-blocking
bool empty() const;
};
Log Entry Structure
Define a structure to hold log information:
struct LogEntry {
LogLevel level;
std::string message;
std::chrono::system_clock::time_point timestamp;
std::thread::id thread_id;
};
Implementation Requirements
Core Features
- Thread-Safe Operations: All logging operations must be safe to call from multiple threads
- Log Levels: Support different severity levels with filtering capability
- Formatted Output: Include timestamps, thread IDs, and log levels in output
- File Output: Support writing to specified output files
- Performance: Minimal overhead on calling threads
Sample Implementation Approach
class Logger {
private:
static std::mutex config_mtx_;
static std::string output_file_;
static LogLevel min_level_;
static ThreadSafeQueue<LogEntry> log_queue_;
static std::thread worker_thread_;
static std::atomic<bool> running_;
static void worker_loop(); // Dedicated logging thread function
public:
static void log(LogLevel level, const std::string& message);
static void initialize(const std::string& filepath, LogLevel level);
static void shutdown();
};
Thread Safety Techniques
Use these synchronization primitives:
- Mutexes: Protect shared data structures
- Condition Variables: Coordinate between producer and consumer threads
- Atomic Operations: For simple shared state like shutdown flags
- RAII Locks: Use
std::lock_guardandstd::unique_lockfor automatic lock management
Advanced Features (Optional)
Consider implementing these additional capabilities:
- Log Rotation: Automatically rotate log files when they reach a certain size
- Asynchronous Flushing: Ensure all pending messages are written before shutdown
- Pattern Formatting: Allow customizable log message formats
- Multiple Outputs: Support logging to both files and console
Performance Considerations
- Minimize lock contention by keeping critical sections small
- Use move semantics to avoid unnecessary copies
- Consider using lock-free queues for high-throughput scenarios
- Profile to verify that logging has minimal impact on application performance
Testing Strategy
Write tests that verify:
- Thread safety with multiple concurrent loggers
- Correct message ordering (or acceptable ordering constraints)
- Proper shutdown handling
- Performance under load
- Error handling for file I/O issues
Usage Example
#include "logger.h"
int main() {
// Initialize logger
Logger::initialize("app.log", LogLevel::INFO);
// Multiple threads logging concurrently
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([i]() {
Logger::info("Message from thread " + std::to_string(i));
Logger::debug("Debug message from thread " + std::to_string(i)); // Won't appear due to level
});
}
// Wait for threads and shutdown
for (auto& t : threads) {
t.join();
}
Logger::shutdown();
return 0;
}
Building and Testing
# Build the project
cd build
cmake ..
make -j4
# The logger should be integrated into the main build
# Test with multi-threaded programs to verify thread safety
Next Steps
After completing the logger, review the Development Standards to understand the coding practices used throughout the project.