muduo网络库源码解析(2):多线程异步日志库(中)

muduo网络库源码解析(1):多线程异步日志库(上)
muduo网络库源码解析(2):多线程异步日志库(中)
muduo网络库源码解析(3):多线程异步日志库(下)
muduo网络库源码解析(4):TimerQueue定时机制
muduo网络库源码解析(5):EventLoop,Channel与事件分发机制
muduo网络库源码解析(6):TcpServer与TcpConnection(上)
muduo网络库源码解析(7):TcpServer与TcpConnection(下)
muduo网络库源码解析(8):EventLoopThreadPool与EventLoopThread
muduo网络库源码解析(9):Connector与TcpClient

引言

上篇文章中分析了muduo日志库的基本流程与同步日志,这篇文章重点介绍异步日志部分,毕竟这才是在多线程编程中主流的方法.

这一篇中只分析一个类,即AsyncLogging,老规矩,首先看一看类的定义

class AsyncLogging : noncopyable
{
 public:

  AsyncLogging(const string& basename,
               off_t rollSize,
               int flushInterval = 3);

  ~AsyncLogging();
  void append(const char* logline, int len);
  void start();
  void stop();

 private:

  void threadFunc(); 

  typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;
  typedef std::vector<std::unique_ptr<Buffer>> BufferVector;
  typedef BufferVector::value_type BufferPtr;

  const int flushInterval_;
  std::atomic<bool> running_;
  const string basename_;
  const off_t rollSize_;
  muduo::Thread thread_;
  muduo::CountDownLatch latch_;
  muduo::MutexLock mutex_;
  muduo::Condition cond_ GUARDED_BY(mutex_);
  BufferPtr currentBuffer_ GUARDED_BY(mutex_);
  BufferPtr nextBuffer_ GUARDED_BY(mutex_);
  BufferVector buffers_ GUARDED_BY(mutex_);
};

我们一个一个来说 首先是数据成员

AsyncLogging::AsyncLogging(const string& basename,
                           off_t rollSize,
                           int flushInterval)
  : flushInterval_(flushInterval), //刷新间隔 默认为三秒 注意一条消息的写入大概是1.2µs左右
    running_(false), //用于线程终止
    basename_(basename),//这两个解析见第一篇中logfile的分析
    rollSize_(rollSize),
    thread_(std::bind(&AsyncLogging::threadFunc, this), "Logging"), //线程实体
    latch_(1), //这个我们一会重点说下
    mutex_(),
    cond_(mutex_),//用于写入这个条件的通知,多个生产者一个消费者
    currentBuffer_(new Buffer),
    nextBuffer_(new Buffer), //double buffers
    buffers_() //buffer集合 用于写入磁盘
{
  currentBuffer_->bzero();
  nextBuffer_->bzero(); //memset
  buffers_.reserve(16); //提前分配空间
}
void AsyncLogging::append(const char* logline, int len)
{
  muduo::MutexLockGuard lock(mutex_);
  if (currentBuffer_->avail() > len) //当前buffer可写长度大于写入长度
  {
    currentBuffer_->append(logline, len);
  }
  else
  {
    buffers_.push_back(std::move(currentBuffer_)); //将当前写入的buffer写入buffers

    if (nextBuffer_) //nextbuffer存在的话补充 
    {
      currentBuffer_ = std::move(nextBuffer_);
    }
    else
    {
      currentBuffer_.reset(new Buffer); // Rarely happens //不存在的话重新申请
    }
    currentBuffer_->append(logline, len); //经过上面的判断currentBuffer_保证存在
    cond_.notify();//条件变量通知
  }
} 

本身这个函数没什么说的,但值得一提的是锁的粒度(锁保护数据量的多少),我们绝对有改变的余地,下面会讲到.我们先来看看核心函数threadFunc,这是异步线程执行的函数,

void AsyncLogging::threadFunc()
{
  assert(running_ == true);
  latch_.countDown(); 
  LogFile output(basename_, rollSize_, false); //也就是说 我们只需要指定output和flush即可使用 即可通过一系列宏函数来使用了
  BufferPtr newBuffer1(new Buffer);
  BufferPtr newBuffer2(new Buffer);
  newBuffer1->bzero();
  newBuffer2->bzero();
  BufferVector buffersToWrite;
  buffersToWrite.reserve(16);
  while (running_)
  {
    assert(newBuffer1 && newBuffer1->length() == 0);
    assert(newBuffer2 && newBuffer2->length() == 0);
    assert(buffersToWrite.empty());

    {
      muduo::MutexLockGuard lock(mutex_); //为了buffers不造成race condition
      if (buffers_.empty())  // unusual usage!
      {
        cond_.waitForSeconds(flushInterval_);// 这里蕴含了两个条件 即超过刷新时间和前方写入一个buffer
      }
      buffers_.push_back(std::move(currentBuffer_));
      currentBuffer_ = std::move(newBuffer1);
      buffersToWrite.swap(buffers_); //指针交换 常数级 也可使用一个vector指针,交换更快
      if (!nextBuffer_)
      {
        nextBuffer_ = std::move(newBuffer2);
      }
    }

    assert(!buffersToWrite.empty()); //显然如果执行到这里无论如何buffersToWrite中也不会为空

    if (buffersToWrite.size() > 25) //日志堆积 即生产者生产速度大于消费者消费速度 进行丢弃日志
    {
      char buf[256];
      snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
               Timestamp::now().toFormattedString().c_str(),
               buffersToWrite.size()-2);
      fputs(buf, stderr);
      output.append(buf, static_cast<int>(strlen(buf)));
      buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end()); 
      //但是仍然保留前两个 避免浪费 因为后面还是要用 
    }

    for (const auto& buffer : buffersToWrite)
    {
      // FIXME: use unbuffered stdio FILE ? or use ::writev ?
      //在第一篇对LogFile的分析中有提到这个问题
      output.append(buffer->data(), buffer->length()); 
    }

    //有意思的是为什么不判断小于2呢 想想看 小于而下面的判断中必然有一个是失败的 也必然会触发assert
    if (buffersToWrite.size() > 2)
    {
      // drop non-bzero-ed buffers, avoid trashing
      buffersToWrite.resize(2);//resize会默认初始化
    }

    if (!newBuffer1)
    {
      assert(!buffersToWrite.empty());
      newBuffer1 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer1->reset();
    }

    if (!newBuffer2)
    {
      assert(!buffersToWrite.empty());
      newBuffer2 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer2->reset();
    }

    buffersToWrite.clear();
    output.flush(); //刷新logfile中的输出 即存储回磁盘
  }
  output.flush(); //线程终止后转储磁盘
}

思考 CountDownLatch是否必要?

核心部分muduo书中已经讲的通透,我想说的只是一点,即CountDownLatch的使用.可能有的朋友不明白为什么我们只有一个异步线程的情况下要使用,我们都知道CountDownLatch可以维护一个happens-before的关系,即执行wait线程之后的语句一定发生在执行countDown线程的语句之后,这代表了什么呢,即一个指定同步的顺序,这里使用的CountDownLatch的作用就是使得在start结束之前进入threadFun函数.但依我拙见,可能并不需要CountDownLatch,两个线程之间通信的唯一桥梁是条件变量与锁,唯一的竞态条件就是如果不用CountDownLatch的话,可能使得在生产者在notify以后Threadfun异步线程没有收到这个信号,但是数据此时仍旧存在生产者线程中,异步线程执行时总会收到消息而把前面的消息写入,就算最极端的情况,生产者只写入了一条消息,仅notify了一次,而这个消息还被异步线程丢失了,由于异步线程中使用的是wait_for,无论如何也会在一个flushInterval_之后被写入,就算时间不足flushInterval_就程序退出,析构函数也会保护最后一次写入,所以也没有关系.综上,我认为在这种单异步线程的代码中,CountDownLatch是不必要的.希望有不同见解的朋友能提出宝贵的意见!

其实关于buffer的情况也很有意思,但我自认为写的一定没有书上详尽,所以那部分还是参考书上的解释为好.

再抛出一个问题,即锁的粒度问题,异步线程处理看来其实也就是一个生产者消费者的模型,那么如何修改的更加高性能呢,即使用更细粒度的锁,线程安全的hashmap.见第三篇.

posted @ 2022-07-02 13:17  李兆龙的博客  阅读(110)  评论(0编辑  收藏  举报