详细介绍:C++ 日志4——多线程异步日志

多线程异步日志系统

3.1 系统的分析:多线程日志的挑战

文档首先清晰地指出了多线程程序对日志库的新需求:

问题所在
  • 线程安全:多个线程并发写日志时,日志消息不能出现交织(一条日志被截断成多段)。

  • 性能瓶颈

    • 全局锁方案:简单的用一个全局mutex保护IO,所有线程争抢同一把锁,性能堪忧。

    • 每线程单独文件:分析日志时需要在多个文件间跳转,极其不便,且不一定能提速。

    • 业务线程阻塞:如果业务线程直接写磁盘,在磁盘IO慢时会被阻塞。

解决方案:异步日志

文档提出了优雅的解决方案:

用一个工作线程负责收集日志消息,并写入日志文件,其他业务线程只需向这个"日志线程"发送日志消息。

这就是"异步日志"的核心思想:将日志的生成写入分离到不同的线程中。

  • 前端(生产者和用线程):只负责生成日志消息,放入缓冲区。

  • 后端(消费者或日志线程):负责将缓冲区中的日志写入文件。

这种架构带来了两大好处:

  1. 非阻塞:业务线程不会被磁盘IO阻塞。

  2. 批处理:减少线程唤醒和系统调用次数。


3.2 系统的设计:双缓冲技术

文档采用了双缓冲(double buffering) 技术,这是实现高性能异步日志的关键。

双缓冲的基本思路

text

准备两块buffer:A和B
前端负责往buffer A填数据(日志消息)
后端负责将buffer B的数据写入文件

当buffer A写满之后:
1. 交换A和B(瞬间完成)
2. 后端将buffer A(现在是旧数据)写入文件
3. 前端往buffer B(现在是空buffer)填入新的日志消息
如此往复
双缓冲的优势
  1. 减少线程同步:前端在大部分时间操作不同的buffer,减少与后端的竞争。

  2. 批处理优化:前端不是一条条传送日志,而是将多条日志拼成大的buffer传送,大幅减少线程唤醒和系统调用次数

  3. 及时性保证:即使buffer未满,也会定期(如3秒)执行交换写入操作,防止日志丢失。

架构示意图

text

前端线程
↓
Log mes→ 4KB Buffer A
Log mes→ 4KB Buffer A
...
↓ (当A满或超时)
交换 A ↔ B
↓
后端线程处理Buffer A(旧数据)
↓
buffersToWrite 数据缓冲队列
↓
文件写缓冲区
↓
磁盘文件

3.3 系统类型的设计

AsyncLogging 类头文件

cpp

namespace tulun {
    class AsyncLogging {
    private:
        AsyncLogging(const AsyncLogging&) = delete;
        AsyncLogging& operator=(const AsyncLogging&) = delete;
        
        void workthreadfunc();    // 工作线程函数
        
    private:
        const int flushInterval_;         // 定期刷新间隔(秒)
        std::atomic running_;       // 是否正在运行
        const string basename_;           // 日志文件名
        const size_t rollSize_;           // 回滚大小
        std::unique_ptr ptthread_;  // 后端线程
        std::mutex mutex_;                // 互斥锁
        std::condition_variable cond_;    // 条件变量
        std::string currentBuffer_;       // 当前前端缓冲区
        std::vector buffers_; // 已满缓冲区队列
        tulun::LogFile output_;           // 日志文件对象
        
    public:
        AsyncLogging(const string& basename, size_t rollSize, int flushInterval = 3);
        ~AsyncLogging();
        void append(const string& info);
        void append(const char* info, int len);
        void start();
        void stop();
        void flush();
    };
}
核心数据成员分析
  1. currentBuffer_

    • 前端线程当前正在写入的缓冲区

    • 类型是std::string,利用其动态扩容特性

  2. buffers_

    • 存储已写满的缓冲区(等待后端线程处理)

    • 使用vector<std::string>作为队列

  3. 同步机制

    • mutex_:保护currentBuffer_buffers_

    • cond_:通知后端线程有数据需要处理

  4. output_

    • 复用之前实现的LogFile,负责实际的文件写入和滚动


3.4 系统的实施和测试

常量和构造函数

cpp

static const int BufMaxLen = 4000;     // 单个缓冲区最大长度
static const int BufQueueSize = 16;    // 缓冲区队列初始大小

AsyncLogging::AsyncLogging(const std::string& basename, size_t rollSize, int flushInterval)
    : flushInterval_(flushInterval),
      running_(false),
      rollSize_(rollSize),
      ptthread_(nullptr),
      output_(basename, rollSize, false) {  // LogFile不需要线程安全,由本类保证
    
    currentBuffer_.reserve(BufMaxLen);      // 预分配内存
    buffers_.reserve(BufQueueSize);         // 预分配vector容量
}

设计要点

  • BufMaxLen = 4000:略小于4KB,与内存页大小对齐

  • 预分配内存避免运行时动态扩容

启动和停止

cpp

void AsyncLogging::start() {
    running_ = true;
    // 创建后端工作线程
    ptthread_.reset(new std::thread(&AsyncLogging::workthreadfunc, this));
}

void AsyncLogging::stop() {
    running_ = false;
    cond_.notify_all();     // 唤醒可能阻塞的线程
    ptthread_->join();      // 等待线程结束
}
前端接口:append方法

这是最关键的生产者代码

cpp

void AsyncLogging::append(const char* info, int len) {
    std::unique_lock _lock(mutex_);
    
    // 如果当前缓冲区剩余空间不足
    if (currentBuffer_.size() >= BufMaxLen || 
        (currentBuffer_.capacity() - currentBuffer_.size()) < len) {
        
        // 将当前缓冲区移到队列中
        buffers_.push_back(std::move(currentBuffer_));
        
        // 重置当前缓冲区(move后currentBuffer_为空)
        currentBuffer_.reserve(BufMaxLen);
        
        // 通知后端线程
        cond_.notify_all();
    }
    
    // 将数据追加到当前缓冲区
    currentBuffer_.append(info, len);
}

关键技术点

  1. std::move优化:避免不必要的内存拷贝

  2. 条件判断:只有当缓冲区满或空间不足时才通知后端

  3. 锁范围:只在操作共享数据时加锁,时间尽可能短

后端线程:workthreadfunc方法

这是最复杂的消费者逻辑

cpp

void AsyncLogging::workthreadfunc() {
    std::vector buffersToWrite;  // 本地队列,减少锁持有时间
    buffersToWrite.reserve(BufQueueSize);
    
    while (running_) {
        {
            std::unique_lock _lock(mutex_);
            
            // 等待条件触发:超时或缓冲区满
            if (buffers_.empty()) {
                cond_.wait_for(_lock, std::chrono::seconds(flushInterval_));
            }
            
            // 无论因何醒来,都将currentBuffer_放入队列
            buffers_.push_back(std::move(currentBuffer_));
            currentBuffer_.reserve(BufMaxLen);
            
            // 交换到本地队列,快速释放锁
            buffersToWrite.swap(buffers_);
            buffers_.reserve(BufQueueSize);  // 保持容量
        }  // 释放锁
        
        // 处理堆积保护:防止生产速度 > 消费速度
        if (buffersToWrite.size() > 25) {
            char buf[256];
            snprintf(buf, sizeof buf, "Dropped log messages at larger buffers\n");
            fputs(buf, stderr);
            // 丢弃多余日志,只保留2个缓冲区
            buffersToWrite.erase(buffersToWrite.begin() + 2, buffersToWrite.end());
        }
        
        // 批量写入文件
        for (const auto& buffer : buffersToWrite) {
            output_.append(buffer.c_str(), buffer.size());
        }
        
        buffersToWrite.clear();
    }
    
    output_.flush();  // 退出前确保所有数据落盘
}

精妙的设计细节

  1. 双重缓冲队列

    • buffers_:全局队列,受mutex保护

    • buffersToWrite:线程本地队列,无锁操作

  2. 锁范围优化

    • 只在交换数据时加锁

    • 文件写入时不持有锁,允许前端继续生产

  3. 堆积处理

    • 当生产速度远超消费速度时,丢弃多余日志

    • 防止内存耗尽导致程序崩溃

  4. 唤醒机制

    • 超时唤醒:保证日志及时性(默认3秒)

    • 数据唤醒:缓冲区满时立即处理

刷新接口

cpp

void AsyncLogging::flush() {
    std::vector buffersToWrite;
    {
        std::unique_lock _lock(mutex_);
        buffers_.push_back(std::move(currentBuffer_));
        buffersToWrite.swap(buffers_);
    }
    
    for (const auto& buffer : buffersToWrite) {
        output_.append(buffer.c_str(), buffer.size());
    }
    output_.flush();
    buffersToWrite.clear();
}

3.5 倒计时门闩优化

文档还介绍了一个优化:使用CountDownLatch确保工作线程真正启动后再返回。

CountDownLatch 实现

cpp

namespace tulun {
    class CountDownLatch {
    public:
        explicit CountDownLatch(int count);
        void wait();
        void countDown();
        int getCount() const;
        
    private:
        mutable std::mutex mutex_;
        std::condition_variable condition_;
        int count_;
    };
}

// 实现
CountDownLatch::CountDownLatch(int count) : count_(count) {}

void CountDownLatch::wait() {
    std::unique_lock _lock(mutex_);
    while (count_ > 0) {
        condition_.wait(_lock);
    }
}

void CountDownLatch::countDown() {
    std::unique_lock _lock(mutex_);
    --count_;
    if (count_ == 0) {
        condition_.notify_all();
    }
}
在AsyncLogging中的使用

cpp

class AsyncLogging {
    // 添加成员
    tulun::CountDownLatch latch_;
    
    // 修改构造函数
    AsyncLogging(...) : ..., latch_(1) {}
    
    // 修改start方法
    void start() {
        running_ = true;
        ptthread_.reset(new std::thread(&AsyncLogging::workthreadfunc, this));
        latch_.wait();  // 等待工作线程真正启动
    }
    
    // 修改workthreadfunc
    void workthreadfunc() {
        latch_.countDown();  // 通知主线程已启动
        // ... 原有逻辑
    }
};

这个优化的意义:防止在AsyncLogging对象析构时,工作线程还未启动完成。


3.6 测试代码

单线程测试

cpp

tulun::AsyncLogging* asynclog = nullptr;

void asyncWriteFile(const string& info) {
    asynclog->append(info);
}

void asyncFlushFile() {
    asynclog->flush();
}

int main() {
    asynclog = new tulun::AsyncLogging("yhping", 1024 * 10);  // 10KB滚动
    tulun::Logger::setOutput(asyncWriteFile);
    tulun::Logger::setFlush(asyncFlushFile);
    asynclog->start();

    for (int i = 0; i < 1000; ++i) {
        LOG_INFO << "main " << i;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    return 0;
}
多线程测试

cpp

void func(char ch) {
    for (int i = 0; i < 1000; ++i) {
        LOG_INFO << "thread " << ch << " count " << i;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    asynclog = new tulun::AsyncLogging("yhping", 1024 * 10);
    tulun::Logger::setOutput(asyncWriteFile);
    tulun::Logger::setFlush(asyncFlushFile);
    asynclog->start();
    
    // 启动多个线程同时写日志
    std::thread tha(func, 'a');
    std::thread thb(func, 'b');
    std::thread thc(func, 'c');
    
    tha.join();
    thb.join();
    thc.join();
    return 0;
}

异步日志系统设计总结

性能优势

  1. 低延迟:业务线程只操作内存缓冲区,不阻塞在磁盘IO

  2. 高吞吐:批量写入减少系统调用次数

  3. 线程安全:通过清晰的锁策略保证数据一致性

可靠性保证

  1. 防堆积:当生产过快时,丢弃日志保护系统

  2. 定期刷新:即使缓冲区未满也定期写入,减少数据丢失风险

  3. 优雅退出:停止时确保所有缓冲数据写入磁盘

工程实践价值

这个异步日志设计是工业级日志库的典型实现,被广泛应用于:

  • 高性能服务器程序

  • 交易系统

  • 实时数据处理系统

它展示了如何通过生产者-消费者模式双缓冲技术批量处理等经典技术解决实际的并发性能问题。

posted @ 2025-12-18 17:15  clnchanpin  阅读(48)  评论(0)    收藏  举报