异步日志

  • 0. buffer
  • 1. 版本1
  • 1.1 介绍
  • 1.2 思路
  • 1.3 分析
  • 1.4 核心代码
  • 1.4.1 改进
  • 1.4.2 最终版本
  • 2. 版本2
  • 2.1 思路
  • 2.2 核心代码
  • 3. [上一篇:简单的日志系统]()


真正的大菜来了。

之前的那个日志滚动仅仅只是一个过渡。前面的实现依靠的是一个全局变量。相当于在依靠一个全局的文件IO锁。当多个线程同时做日志时,会抢一个锁浪费时间。
举例
A线程写日志,抢到锁后,向文件写。
此时其他所有线程如果写日志,则需要在这里同时阻塞。这简直就是灾难。

而异步日志(准确来说是非阻塞日志)恰恰可以解决这个问题。这个类型的问题甚至可以被这种模型解决。

其准确思想就是分为将这个日志系统分为C/S架构。

一个日志线程被我们称为后端。所有在打日志的线程称为前端。
前端线程并非每打一次日志都通知后端,而是将所有的日志信息发送到一个队列上。然后后端线程从这个队列上接收数据而后写到文件上。

队列是一种抽象的说法,实际上使用的是双缓冲技术。基本思路就是前端负责向A填数据(日志消息),后端负责将buffer B的数据写入文件。当buffer A写满之后,就交换A和B。让后端将buffer A的数据写入文件,而前端则往buffer B中继续写。同时设计一个定时时间,即前端没有写满数据,时间一到,也会交换buffer。

0. buffer

前面的设计中,我们都用到了buffer这么一个概念。但是仅仅使用string来做了替代。现在我们特别抽象出一个buffer来。

其学习自muduo::LogStream中的FixBuffer。非常简单。

  1. 用模板特例化。FixBuffer<1024>类似array的方式使用。
  2. append。
  3. 其他的都是STL常用接口。

1. 版本1

1.1 介绍

使用最简单的双缓冲区设置,用来应对日常环境我认为足以。文章中给出的代码一般代表我思考的过程,最终的github地址给出的代码则是最终成型后的版本。我认为coding过程中,不断思考,不断改进才是coding最有趣的地方

1.2 思路

思路其实非常简单,就是设置俩个Buffer,日志一开始输出到currentBuffer上去,当currentBuffer满了之后。发生交换。(我们这里仅仅交换指针,这里有个小坑请注意,即数组名作为指针是不能发生交换的,需要做一个假的dummy指针)

char data[SIZE];
char * dummy = data;
char * cur = data;

// 发生交换时
swap(dummy, rhs.dummy);
swap(cur, rhs.cur);
// swap(data, rhs.data); 是一个无效操作

交换后,使用条件变量通知后端线程,后端线程唤醒,然后开始将backBuffer中的数据向文件中写。写完毕后,记得要reset backBuffer的cur指针,即cur = dummy(backBuffer此时指向的数组并非data)。

1.3 分析

这个模型比较有趣。先思考我们的LogFile这个模型。
这个模型就是,一条日志触发一次写操作。
我们都知道,磁盘的读写是非常耗时的,高频率的磁盘读写操作会十分的降低我们程序的性能。

AsyncLogging则将这个模型中间添加了一个buffer。一条日志仅仅触发一次向buffer中的添加操作。当buffer满了的时候才会去触发一次写操作。
将原来的一条日志触发一次写改为了多条日志触发一次写,有效地降低了磁盘写的频率。

1.4 核心代码

// 我这里故意写了是为了方便进行测试,这个buffer的大小可以自己设定
FixBuffer<4*100*1024> currentBuffer;
FixBuffer<4*100*1024> backBuffer;
MutexLock mutex; // 用来保护append操作和writeToFile操作

// append操作
void
AsyncLogging::append(const char*data, int len)
{
    MutexLockGuard lock(mutex);
    if(currentBuffer.avail() > len)
    {   // 最常见的操作
        currentBuffer.append(data, len);
    }
    else{ // currentBuffer满了,需要进行swap
        currentBuffer.swap(backBuffer);
        // TODO 验证交换过来的currentBuffer
        currentBuffer.append(data, len);
        cond.notify();
    }
    
}

// 后端线程写,所以是一个单独线程操作
void 
AsyncLogging::writeToFile()
{
    LogFile logFile(basename, rollSize, interval);
    while(isRunning)
    {
        MutexLockGuard lock(mutex);
        if(backBuffer.empty()) // 这里用了if,是希望interval事件到了,即使没有写的东西,也去触发日志滚动。
            cond.waitForSecond(interval);
        logFile.append(backBuffer.data(), backBuffer.length());
        logFile.flush(); // 这个可以移动到锁外面
        backBuffer.reset();
    }
}
1.4.1 改进

不难发现,我们后端这个writeToFile这里的临界区是有点长的。
可以的改进就是当cond被唤醒之后,我们将backBuffer中的数据移动到栈区域。然后backBuffer就可以reset了。

void 
AsyncLogging::threadFunc()
{
  assert(isRunning_ == true);
  latch_.countDown();
  LogFile logFile(basename_, rollSize_, interval_);
  LargeBuffer newBuffer; // 做成成员变量更好一些
  while(isRunning_)
  {
    {
      MutexLockGuard lock(mutex_);
      // 如果backBuffer_为empty,后端等待
      if(!backBuffer_.length()) // 注意这里使用的是if而不是while,
                                // 我们希望interval时间到了之后,不再等待
        cond_.waitForSeconds(interval_);
      // 将backBuffer中的数据交给newBuffer
      // 这里实际上也可以用swap来做
      newBuffer.swap(backBuffer_);
    }
    logFile.append_unlocked(newBuffer.data(), newBuffer.length()); 
    newBuffer.reset();
    logFile.flush();
  }
  // 这里可能会有认为存在一个析构先后关系的问题。即newBuffer析构的时候,此时其实际data存储的数据
  // 可能是其他,如果此时再次执行日志输出,可能会引发segment fault。
  // 1. stop的需要一些设计。要保证stop的时候,其他线程不能在append
  // 2. 或者可以将newBuffer做成AsyncLogging的成员。注释中要表明其只能被后端线程使用。
}

其本质上如这幅图表示。有三段内存。

currBuffer满了,此时和backBuffer进行交换

线程池打印日志java 多线程打印日志_临界区

交换完毕后,currBuffer变空了,backBuffer变满了

线程池打印日志java 多线程打印日志_c++_02

然后backBuffer和newBuffer进行交换

线程池打印日志java 多线程打印日志_线程池打印日志java_03

最终结果就是newBuffer变满了,然后将里面的数据写向文件。而currBuffer又可以append数据。因为newBuffer是不会暴露给其他线程的。所以其可以在临界区域外执行。注意要在其内数据写向文件后,进行reset中心变回最开始的状态。

其中俩段临界区内均只是指针的变化。所以非常高效。
那么限制这里log速度最终瓶颈就变成了磁盘的写速度。这就不是编程能解决的问题了。
但是这里有个缺陷,就是在interval时间到达的时候,我们希望其应该将currBuffer中的数据也应该输出到文件中去。而这里是没有的。

1.4.2 最终版本
void 
AsyncLogging::threadFunc()
{
  assert(isRunning_ == true);
  latch_.countDown();
  LogFile logFile(basename_, rollSize_, interval_);
  LargeBuffer newBuffer;
  newBuffer.reset();
  while(isRunning_)
  {
    {
      MutexLockGuard lock(mutex_);
      // 如果backBuffer_为empty,后端等待
      if(!backBuffer_.length()) // 注意这里使用的是if而不是while,
                                // 我们希望interval时间到了之后,不再等待
        cond_.waitForSeconds(interval_);
      // 将backBuffer中的数据交给newBuffer
      if(backBuffer_.length())
      {
        newBuffer.swap(backBuffer_);
      }
      else{ // 因为时间到了唤醒
        newBuffer.swap(currentBuffer_);
      }
    }
    logFile.append_unlocked(newBuffer.data(), newBuffer.length()); 
    newBuffer.reset();
    logFile.flush();
  }
}

2. 版本2

这个是muduo实现的版本。muduo使用的实际上是4缓冲区的方式。前提是需要boost::指针容器中的一些知识。
这里给出参考:

  1. 英语好的直接去看boost官方文档
  2. 英语不好的去看《boost程序库探秘》

2.1 思路

muduo采用的是所谓4缓冲区的方式。
append思路

  1. 如果currBuffer足够大,就直接currBuffer.append(data, len);
  2. 如果不够,就将其按照移动语义的方式扔到个buffer指针数组中去。然后把nextBuffer调上来。
  3. 如果nextBuffer也被用了,就去新开辟一个buffer。

writeToFile思路。

  1. 临界区内,按照移动语义方式将currBuffer扔到指针数组中去。
  2. 临界区内,填充currBuffer和nextBuffer
  3. 临界区内,将指针数组中的指针交换到局部变量中去。
  4. 临界区,开始写操作。然后进行琐碎的收尾工作。

知道大概思路,而且掌握了指针容器,这里才好写

2.2 核心代码

MutexLock mutex;
Condition cond;
using BufferPtrVector = boost::ptr_vector<LargeBuffer>;
using BufferMovablePtr = BufferPtrVector::auto_type;

BufferMovablePtr currBuffer;
BufferMovablePtr nextBuffer;
BufferPtrVector buffers;

// BufferMovablePtr的使用方式类似unique_ptr
void
AsyncLogging::append(const char*data, int len)
{
    MutexLockGuard lock(mutex);
    if(currBuffer->avail() > len)
        currBuffer->append(data, len);
    else{
        // 放弃currBuffer的拥有权
        buffers.push_back(currBuffer.release());
        if(nextBuffer)
            currentBuffer = ptr_container::move(nextBuffer);
        else{   // 此时currentBuffer内部没有指针管理
            currentBuffer.reset(new LargeBuffer);
        }
        currBuffer->append(data, len);
        cond.notify();
    }
}

void 
AsyncLogging::writeToFileThread()
{
    BufferMovablePtr newBuffer1(new LargeBuffer);
    BufferMovablePtr newBuffer2(new LargeBuffer);
    BufferPtrVector localBuffers;
    LogFile log(basename, rollSize, interval);
    while(running)
    {
        {
            MutexLockGuard lock(mutex);
            if(buffers.empty())
                cond.waitForSeconds(interval);
            buffers.push_back(currBuffer.release());
            localBuffers.swap(buffers);
            currBuffer = ptr_container::move(newBuffer1);
            if(!nextBuffer) // nextBuffer被用完了
                nextBuffer = ptr_container::move(newBuffer2);
        }
        for(auto iter = localBuffer.begin(); iter != localBuffer.end(); ++iter)
        {
            log.append(iter->data(), iter->length());
            log.fflush();
        }
        localBuffers.reset(); // 释放所有指针的伪代码,简单做法
        newBuffer1.reset(new LargeBuffer);
        newBuffer2.reset(new LargeBuffer);
    }
}

就localBuffer的释放指针操作而言,muduo这里有非常优雅的做法。

if(localBuffers.size() > 2)
    localBuffer.resize(2);
newBuffer1 = ptr_vector::move(localBuffers.front());
newBuffer2 = ptr_vector::move(localBuffers.back());
newBuffer1->reset();
newBuffer2->reset();
localBuffer.pop_back();
localBuffer.pop_back();
localBuffer.clear();

3. 上一篇:简单的日志系统