Boost.ASIO源码:deadline_timer源码级解析(三)—— 从源码解释io_service::run()到底发生了什么

前前后后run这个函数来来往往反反复复看了不知道多少遍,对它的逻辑始终没弄明白,直到最近研究deadline_timer才恍然大悟理清了前面的一些逻辑,在此顺便总结一下,也算是填了前面几个博客一直没讲明白的一些点。(因为那时我还没完全看懂,故只能留坑了。。)
我前面所有的博客都算是这篇博客的铺垫,本文中也多次引用了我以前写的博客,需要的时候建议还是参考下,不然可能有点难以理解。

前文回顾

前面两个博客一个讲了deadline_timer的调用逻辑,一个讲了epoll_reactor的触发逻辑。其实最重要的真正处理逻辑一直没讲,而这个处理逻辑,便是在io_service::run()中。
前面两篇文章,第一篇是讲到epoll_reactor::schedule_timer就没有扩展下去了,第二篇在第一篇的基础上进行扩展,但讲到epoll_reactor::run()也没有扩展下去了。在这里我还是建议一下至少先看前面两篇文章,虽然直接看这篇文章也能对io_service::run()有所了解,但是从最外层调用开始看能对整个代码逻辑有更宏观的理解。

io_service::run的背景相关

这些其实大部分前面的博客都说过了,但在这里还是总结说明一下。
io_service是一个别名,它的本名叫io_context,而io_context继承自execution_context,代表一个上下文环境,这个execution_context持有一个service_registry对象,service_registry维护所有的服务,每种服务只会在service_registry中存在一个对象,这个主要通过use_service这个函数实现。而服务说直白点,就是调用ASIO的那些接口类所调用的逻辑处理函数集合体(当然服务里还会持有相关数据),所有的服务都继承自execution_context::service,这个基类规定,所有的服务都要持有它的运行上下文对象,即execution_context(具体点方便理解,这里就把execution_context直接看作io_service就行了)。而io_context实际上也是不干实事的,io_context有个成员叫impl_,这个成员可以理解为io_context的具体逻辑实现类,io_context几乎所有函数都是调用这个impl_的接口函数。在这里,这个impl_就是我们前面多次提到的scheduler
所以所谓的io_service.run,它的真实逻辑在scheduler.run中。

从构造scheduler到scheduler::run()到底发生了什么

这里还提到了构造scheduler,scheduler在构造时实际上还做了一些很不明显的工作,如果忽略的话scheduler::run()的逻辑就讲不通了(这也是前面我一直没看懂的原因)。
首先看到一个最基本的deadline_timer用法,后面会以这个例子为例来讲解run的逻辑:

void Print(const boost::system::error_code &ec);
boost::asio::io_service io;  
boost::asio::deadline_timer t(io, boost::posix_time::seconds(5));  
t.async_wait(Print);  
io.run()

从第2行构造io_service看起,此时当然实际上执行的是io_context的空参构造函数,其中会以默认的方式构造scheduler:

io_context::io_context()
  : impl_(add_impl(new impl_type(*this, BOOST_ASIO_CONCURRENCY_HINT_DEFAULT)))// 这个impl_type就是scheduler的别名
{
}

io_context::impl_type& io_context::add_impl(io_context::impl_type* impl)  // 这个impl_type就是scheduler的别名
{
  boost::asio::detail::scoped_ptr<impl_type> scoped_impl(impl);  // 指针包裹类,销毁时会delete指针所指对象
  boost::asio::add_service<impl_type>(*this, scoped_impl.get());  // 这里把scheduler添加到service_registry中
  return *scoped_impl.release();  // 解除指针与所指对象的绑定关系,这样就不会释放原本所指的对象了。
  // 为什么既然最后要解除绑定还要用scoped_impl这种智能指针来处理呢:考虑下中间异常退出的情况。
}

然后再看所调用的scheduler构造函数:

scheduler::scheduler(
    boost::asio::execution_context& ctx, int concurrency_hint)
  : boost::asio::detail::execution_context_service_base<scheduler>(ctx),
    one_thread_(concurrency_hint == 1
        || !BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)
        || !BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          REACTOR_IO, concurrency_hint)),
    mutex_(BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)),
    task_(0),  // 这个是epoll_reactor,此时初始化为空
    task_interrupted_(true),  // 一个flag,标识epoll_reactor是否处于中断状态
    outstanding_work_(0),  // 未完成的任务数
    stopped_(false),
    shutdown_(false),
    concurrency_hint_(concurrency_hint)
{
}

这里只需要记住outstanding_work_这个属性就行了,这个表示未完成任务数的成员属性将是决定scheduler::run()的运行状态的关键属性。
这里可以看到在scheduler构造时实际上它的触发器是空的,那这个epoll_reactor是在哪里初始化呢——在deadline_timer的构造函数中(这里仅仅是以最前面那个例子为例,并不是说epoll_reactor只能在这里面初始化,准确说epoll_reactor在许多相关的服务类中都会触发初始化)。如前面文章所说,deadline_timer的构造实际上会导致它的服务类deadline_timer_service的构造,而deadline_timer_service的构造函数中会调用epoll_reactor::init_task(),而这个函数又会调用scheduler::init_task():

  deadline_timer_service(boost::asio::io_context& io_context)
    : service_base<deadline_timer_service<Time_Traits> >(io_context),
      scheduler_(boost::asio::use_service<timer_scheduler>(io_context))
  {
    scheduler_.init_task();  // here~
    scheduler_.add_timer_queue(timer_queue_);
  }

void epoll_reactor::init_task()
{
  scheduler_.init_task();
}

void scheduler::init_task()
{
  mutex::scoped_lock lock(mutex_);
  if (!shutdown_ && !task_)
  {
    task_ = &use_service<reactor>(this->context());
    op_queue_.push(&task_operation_);
    wake_one_thread_and_unlock(lock);
  }
}

这里需要强调注意的是scheduler::init_task中的task_operation_,op_queue是scheduler的所有待处理回调函数(包装类)队列,而这个task_operation_是个空的包装类,仅代表每个scheduler它的epoll_reactor的占位符。

  // Operation object to represent the position of the task in the queue.
  struct task_operation : operation
  {
    task_operation() : operation(0) {}
  } task_operation_;

当从队列中取出的op是这个task_operation_时,则说明scheduler该处理epoll_reactor了。(不然谁来调用epoll_reactor::run()啊)
然后接着执行例子中的async_wait函数,参考前面的博客,这时候会执行到epoll_rector::schdule_timer函数,当然,此时shutdown_标识为为false,故走的是下面的逻辑:

template <typename Time_Traits>
void epoll_reactor::schedule_timer(timer_queue<Time_Traits>& queue,
    const typename Time_Traits::time_type& time,
    typename timer_queue<Time_Traits>::per_timer_data& timer, wait_op* op)
{
  mutex::scoped_lock lock(mutex_);

  if (shutdown_)
  {
    scheduler_.post_immediate_completion(op, false);
    return;
  }

  bool earliest = queue.enqueue_timer(time, timer, op);
  scheduler_.work_started();
  if (earliest)
    update_timeout();
}

// scheduler.hpp
  void work_started()
  {
    ++outstanding_work_;
  }

shutdown_为true的情况后面会讲。这里执行完后就返回了,实际上变化的只有epoll_reactor的定时器队列,还有scheduler的outstanding_work_未完成任务数以及epoll中timerfd的定时触发时间。再次强调,注意此时outstanding_work_不再是0了(在这里为1)。
下面就是最关键的io_service::run()了,也就是例子中的 io.run() 。当然,此时调用的是scheduler.run():

std::size_t scheduler::run(boost::system::error_code& ec)
{
  ec = boost::system::error_code();
  if (outstanding_work_ == 0)
  {
    stop();
    return 0;
  }

  thread_info this_thread;
  this_thread.private_outstanding_work = 0;
  thread_call_stack::context ctx(this, this_thread);

  mutex::scoped_lock lock(mutex_);

  std::size_t n = 0;
  for (; do_run_one(lock, this_thread, ec); lock.lock())
    if (n != (std::numeric_limits<std::size_t>::max)())   // 如果n还没到最大值:防止溢出
      ++n;
  return n;
}

还记得我前面强调的在调用run()时outstanding_work_不为0吗,不然此时就直接stop()了。所以说io_service在调用run()之前一定要先添加任务。
thread_info代表当前的私有工作线程,具体逻辑参考我前面的博客,然后便是循环调用do_run_one了,这个函数每次处理一个op_queue_中的项:

std::size_t scheduler::do_run_one(mutex::scoped_lock& lock,
    scheduler::thread_info& this_thread,
    const boost::system::error_code& ec)
{
  while (!stopped_)
  {
    if (!op_queue_.empty())   // 非空就取出来一个处理,空的话就阻塞等待唤醒
    {
      // Prepare to execute first handler from queue.
      operation* o = op_queue_.front();
      op_queue_.pop();
      bool more_handlers = (!op_queue_.empty());

      if (o == &task_operation_)   // 这就是我提到的task_operation_作为epoll_reactor的占位符
      {
        task_interrupted_ = more_handlers;

        if (more_handlers && !one_thread_)   // 如果还有其它任务要执行,且当前时多线程执行环境
          wakeup_event_.unlock_and_signal_one(lock);  // 唤醒另外一个线程来执行其它任务,本线程接着处理epoll_reactor
        else
          lock.unlock();

        task_cleanup on_exit = { this, &lock, &this_thread };   // 这个对象仅仅是利用它的析构函数
        (void)on_exit;   // 编译器欺骗手法,防止报警告

        task_->run(more_handlers ? 0 : -1, this_thread.private_op_queue);  // 这里调用epoll_reactor::run
      }
      else
      {
      
        // 这里处理其它的需执行的op_queue_的项

        return 1;
      }
    }
    else
    {   // 如果op_queue_为空,则阻塞
      wakeup_event_.clear(lock);
      wakeup_event_.wait(lock);
    }
  }

  return 0;
}

总的来说,先从op_queue_中取出待操作项,如果op_queue_为空,则将该线程阻塞(wakeup_event_细节参考前面的博客)。
在本文所介绍的例子中,调用run()时op_queue_中只有一项,就是task_operation_这个epoll_reactor的占位符,故接下来会调用epoll_reactor::run,然后epoll_reactor::run函数会阻塞直到定时器到点,然后会将定时器的回调函数op添加到私有队列this_thread.private_op_queue中并返回(这一段内容可参考上一篇博客)。
返回后私有队列有我们将处理的回调函数(包装类),但op_queue_是空的,那究竟在哪里把私有队列中的op转移到op_queue_中呢,这里用了很巧妙的方法,就是这个task_cleanup。可以看到on_exit变量实际上并没有用到,准确说我们只用到了它的析构函数,在if语句块结束时就自动执行了,哪怕出现了异常也会执行(这才是重点)。如下为task_cleanup的源码,它是scheduler的内部类:

struct scheduler::task_cleanup
{
  ~task_cleanup()
  {
    if (this_thread_->private_outstanding_work > 0)
    {
      boost::asio::detail::increment(
          scheduler_->outstanding_work_,
          this_thread_->private_outstanding_work);
    }
    this_thread_->private_outstanding_work = 0;

    // Enqueue the completed operations and reinsert the task at the end of
    // the operation queue.
    lock_->lock();
    scheduler_->task_interrupted_ = true;
    scheduler_->op_queue_.push(this_thread_->private_op_queue);
    scheduler_->op_queue_.push(&scheduler_->task_operation_);
  }

  scheduler* scheduler_;
  mutex::scoped_lock* lock_;
  thread_info* this_thread_;
};

可以看到这个析构函数的做法就是把this_thread_中的private_op_queue中的任务(还是叫任务吧,实际上里面就是回调函数的包装类)转移到scheduler的op_queue_中同时把scheduler的task_interrupted标为true,因为执行到这里task_.run已经返回了,相当于epoll_reactor暂时处于不运行状态。 这里再注意一个细节,把this_thread中的私有任务(private_op_queue)转移到scheduler中后,还重新把task_operation添加到队列尾端。这是为了保证所有的人物全部执行之后,epoll_reactor再重新运行。再参考如下epoll_reactor::run中的官方注释就不难理解了(不要偷懒,我建议还是好好读下这段英文):

  // This code relies on the fact that the scheduler queues the reactor task
  // behind all descriptor operations generated by this function. This means,
  // that by the time we reach this point, any previously returned descriptor
  // operations have already been dequeued. Therefore it is now safe for us to
  // reuse and return them for the scheduler to queue again.

再回到scheduler::do_run_one中,if结束后op_queue_中就有人物了,在下一次循环时就自然进入到了上面源码中省略的else的语句块中,下面再把这段源码补上:

//。。。
	   if (o == &task_operation_)
       {
        // 。。。
      }
      else
      {
        std::size_t task_result = o->task_result_;

        if (more_handlers && !one_thread_)
          wake_one_thread_and_unlock(lock);
        else
          lock.unlock();

        // Ensure the count of outstanding work is decremented on block exit.
        work_cleanup on_exit = { this, &lock, &this_thread };
        (void)on_exit;

        // Complete the operation. May throw an exception. Deletes the object.
        o->complete(this, ec, task_result);   // 这里就是处理任务(执行回调函数)了

        return 1;
      }
//。。。

终于看到最终任务的处理代码了,就是那行complete函数的执行。可以看到这里用到了work_cleanup,猜也猜得到那是task_cleanup类似的用法,接下来直接看work_cleanup的源码:

struct scheduler::work_cleanup
{
  ~work_cleanup()
  {
    if (this_thread_->private_outstanding_work > 1)
    {
      boost::asio::detail::increment(
          scheduler_->outstanding_work_,
          this_thread_->private_outstanding_work - 1);
    }
    else if (this_thread_->private_outstanding_work < 1)
    {
      scheduler_->work_finished();
      // void work_finished()
	  // {
	  //   if (--outstanding_work_ == 0)
	  //     stop();
	  // }
    }
    this_thread_->private_outstanding_work = 0;

    if (!this_thread_->private_op_queue.empty())
    {
      lock_->lock();
      scheduler_->op_queue_.push(this_thread_->private_op_queue);
    }
  }

  scheduler* scheduler_;
  mutex::scoped_lock* lock_;
  thread_info* this_thread_;
};

在本文的例子中,析构函数刚开始private_outstanding_work是等于0的,故直接把scheduler的未完成任务数(outstanding_work)减一,此时scheduler的outstanding_work也为0了(本来就只有Print那一个任务),故执行stop(),此时scheduler::stopped_标志位就变为true了,下次scheduler::do_run_one就会跳出循环并返回0了,而scheduler::run中循环调用do_run_one的那个for循环自然也就结束了。
为了补全上述代码的逻辑,这里再提一下这个stop函数,下面看源码:

void scheduler::stop()
{
  mutex::scoped_lock lock(mutex_);
  stop_all_threads(lock);
}

void scheduler::stop_all_threads(
    mutex::scoped_lock& lock)
{
  stopped_ = true;
  wakeup_event_.signal_all(lock);

  if (!task_interrupted_ && task_)
  {
    task_interrupted_ = true;
    task_->interrupt();
  }
}

对于wakeup_event的细节可以参考我前面的博客。
可以看到这里实际上通知了所有的线程scheduler应该结束do_run_one函数的循环调用了,还记得do_run_one中若op_queue_为空便会阻塞在wakeup_event_.wait()上吗,对于多个线程执行同一个io_service的run时,这是经常发生的情况:当前只有一个任务,故只有一个线程处于正常的执行状态,其它的都阻塞在wait()上,当那个正常执行的线程执行完毕后,stopped_标志位也更新为true了,此时这个线程再执行wakeup_event_.signal_all唤醒所有阻塞在wait上面的线程,这些线程发现stopped_为true自然也就结束自己的run函数的执行了,由此达到目的。
综上所述,可以看到,在scheduler中task和work的概念实际上已经比较清晰了——task指epoll_reactor这个调度器,而work指正常的外界传入的回调函数这样的任务(当初还不懂完整逻辑时为了理解这一茬可是花了我好久好久)。

回到deadline_timer

在我的deadline_timer源码级解析(一)一文中最后降到了epoll_reactor::schedule_timer函数:

template <typename Time_Traits>
void epoll_reactor::schedule_timer(timer_queue<Time_Traits>& queue,
    const typename Time_Traits::time_type& time,
    typename timer_queue<Time_Traits>::per_timer_data& timer, wait_op* op)
{
  mutex::scoped_lock lock(mutex_);

  if (shutdown_)
  {
    scheduler_.post_immediate_completion(op, false);
    return;
  }

  bool earliest = queue.enqueue_timer(time, timer, op);//将定时器添加进队列,这个队列是deadline_timer_service的timer_queue_成员
  scheduler_.work_started();
  if (earliest)
    update_timeout();  // ,如果当前定时器的触发时间最早,则更新epoll_reactor的timer_fd
}

其中第8到12行的逻辑还没讲,在这里已经有schduler::run运行逻辑的铺垫后,这个post_immediate_completion也就不难理解了。先来看post_immediate_completion的源码:

  // Request invocation of the given operation and return immediately. Assumes
  // that work_started() has not yet been called for the operation.
void scheduler::post_immediate_completion(
    scheduler::operation* op, bool is_continuation)
{
  if (one_thread_ || is_continuation)
  {
    if (thread_info_base* this_thread = thread_call_stack::contains(this))
    {
      ++static_cast<thread_info*>(this_thread)->private_outstanding_work;
      static_cast<thread_info*>(this_thread)->private_op_queue.push(op);
      return;
    }
  }

  work_started();
  mutex::scoped_lock lock(mutex_);
  op_queue_.push(op);
  wake_one_thread_and_unlock(lock);
}

先不看if内的逻辑(deadline_timer也不走这个if中的逻辑),从函数名的immediate就可以看出,这相当于立即处理的方式——直接把任务push到scheduler的op_queue_中,不走this_thread->private_op_queue那一层了,这样子也就不需要epoll_reactor::run中的逻辑进行处理了,再回想下先开始就是判断epoll_reactor::shutdown_为true时才走post_immediate_completion函数的,理到这里逻辑就清晰了。
这时候再来看if中的逻辑,这里的is_continuation的意思着实困惑了我很久,最后理解了半天我才感觉将这个continuation翻译为延迟比较合适——如果is_continuation为true代表延迟执行。为什么这么说呢,可以看到这里面先判断当前线程是否处于调用栈中,何谓调用栈,就是当前这个执行上下文中的scheduler有没有调用run函数,再回想下scheduler::run中会声明一个this_thread变量,此时就相当于把该scheduler添加到调用栈中了。其中具体逻辑参考我前面的博客。如果是再调用栈中,就把它的thread_info取出来,然后把该任务添加到它的thread_info的private_op_queue中再返回。此时就相当于要等epoll_reactor运行起来后,再把这个private_op_queue中的任务添加到op_queue_中,后面就是正常的处理逻辑了。这就是我为什么把这个continuation翻译为延迟的原因。

关于io_service私有队列公共队列的逻辑总结

这里相当于重新说明一下前面的private_op_queue和scheduler的op_queue_的逻辑。因为前面所说的逻辑好像不符合正常对io_service的理解:private_op_queue不是私有队列吗,op_queue_不是公有队列吗,为什么好像是把私有队列中的任务放到公有队列中,而不是把公有队列中的任务放到私有队列中?
其实这个问题我也纳闷了一段时间,但只要仔细理解代码逻辑就能发现,private_op_queue的主要任务是暂存将要处理的任务,至于为什么叫private_op_queue呢,是因为这个将要处理的任务是由某个线程发现的,是它在它自己的scheduler::run中的epoll_reactor::run中的某处逻辑发现的(再仔细回想下,如果要用到io_service多线程实际上是要自己创建多个线程来调用同一个io_service::run的,此时每个io_service::run中都会有自己的临时变量this_thread,而这个thread_info类型的变量某种程度上就代表了属于某个工作线程的私有数据信息,private_op_queue就是它的成员)。而某个正运行scheduler::run的线程要处理这些任务并不是直接从它自己的private_op_queue中取出来,而是先放到所有线程共享的同一个scheduler::op_queue_中,再将任务从中取出来处理。
为了防止逻辑混乱这里再重申一下,哪些数据是属于多线程共享的,哪些是数据是属于每个线程私有的(这里指的多线程是指多个运行同一个io_service对象的run函数的线程)。所有scheduler中的成员变量都是共享的,所有scheduler::run中的临时变量,主要就是thread_info类型的this_thread变量是私有的

你可能感兴趣的:(源码阅读笔记)