欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Boost Asio要点概述(二)

程序员文章站 2022-05-31 20:18:46
...

三、多任务执行

复杂一些的应用,往往存在多个事件任务执行,此时既有可任意顺序执行的场景,也有按指定顺序执行的场景,前者可采用将任务放到多个线程执行,后者要用到”strand”概念。

1.多线程支持

Boost中io_context是支持多线程的,其内部有一个队列来分配待运行的句柄函数,对服务器端程序来说,这减轻了我们采用多线程同时执行任务的难度,虽然我们不能控制任务在多个线程进行分配的策略。

因此,对服务器端编程,多线程大致的流程如下:

  • 创建一个io_context(n),n是大致要运行的多线程的格式(关于n的意义,参见Boost文档中关于Concurrency Hints的解释,为提高性能,也可以关闭任务分布时的加锁机制)
  • 创建并启动多个线程,每个线程中调用io_context.run(),此时多个线程组成线程池。
  • 事件函数会在多个线程中执行,当然如果有资源的竞争,锁和同步机制必不可少。

一个简单示例如下。结果是timer1的运行和timer2的运行放到了两个线程。

int main()
{
  io_context ioservice;

  steady_timer timer1{ioservice, std::chrono::seconds{3}};
  timer1.async_wait([](const boost::system::error_code &ec)
    { std::cout << "3 sec\n"; });

  steady_timer timer2{ioservice, std::chrono::seconds{3}};
  timer2.async_wait([](const boost::system::error_code &ec)
    { std::cout << "3 sec\n"; });

  std::thread thread1{[&ioservice](){ ioservice.run(); }};
  std::thread thread2{[&ioservice](){ ioservice.run(); }};
  thread1.join();
  thread2.join();
}

2.按顺序执行

有些场景下需要将事件顺序运行,但通常情况下,多线程时io_context自动分配任务,后面的任务可能会放到另外的线程中执行,从而并不能保证任务执行的先后顺序。

Asio中,一个strand表示其中的事件能以”Post”的顺序被调用,即使在多线程环境中也是如此。”strand”是一个”executor”,将事件与一个strand绑定,表示事件只能由这个executor执行,也就变成串行了。以下是一个strand的示例。

class printer
{
public:
  printer(boost::asio::io_context& io)
    : strand_(io),
      timer1_(io, std::chrono::seconds(1)),
      timer2_(io, std::chrono::seconds(1)),
      timer3_(io, std::chrono::seconds(1)),	  	  
      count_(0)
  {
	timer1_.async_wait(boost::asio::bind_executor(strand_,std::bind(&printer::print1, this)));
    timer2_.async_wait(boost::asio::bind_executor(strand_, std::bind(&printer::print2, this)));
    timer3_.async_wait(std::bind(&printer::print3, this));		
  }
  void print1()
  {
    if (count_ < 20)
    {
      std::cout << "Timer 1: " <<std::this_thread::get_id()<<"," << count_ << std::endl;
      ++count_;
      timer1_.expires_after(std::chrono::seconds(1));
      timer1_.async_wait(boost::asio::bind_executor(strand_,
            std::bind(&printer::print1, this)));
    }
  }
  void print2()
  {
    if (count_ < 20)
    {
      std::cout << "Timer 2: " <<std::this_thread::get_id()<<"," << count_ << std::endl;
      ++count_;
      timer2_.expires_after(std::chrono::seconds(1));
      timer2_.async_wait(boost::asio::bind_executor(strand_, std::bind(&printer::print2, this)));
    }
  }
  void print3()
  {
    if (count_ < 20)
    {
      std::cout << "Timer 3: "<<std::this_thread::get_id()<<"," << count_ << std::endl;
      ++count_;
      timer3_.expires_after(std::chrono::seconds(1));
      timer3_.async_wait(std::bind(&printer::print3, this));
    }
  }
private:
  boost::asio::io_context::strand strand_;
  boost::asio::steady_timer timer1_;
  boost::asio::steady_timer timer2_;
  boost::asio::steady_timer timer3_;  
  int count_;
 }; 

int main()
{
  boost::asio::io_context io;
  printer p(io);
  std::thread t([&io](){ io.run();});
  io.run();
  t.join();
  return 0;
}
输出:
Timer 1: 139917122738048,0
Timer 2: 139917122738048,1
Timer 3: 139917104916224,2
Timer 1: 139917122738048,3
Timer 2: 139917122738048,4
Timer 3: 139917104916224,5
Timer 1: 139917122738048,6
Timer 2: 139917122738048,7
Timer 3: 139917122738048,8
Timer 1: 139917122738048,9
Timer 2: 139917122738048,10
Timer 3: 139917104916224,11
Timer 1: 139917122738048,12
Timer 2: 139917122738048,13
Timer 3: 139917122738048,14
Timer 1: 139917104916224,15
Timer 2: 139917104916224,16
Timer 3: 139917122738048,17
Timer 1: 139917104916224,18
Timer 2: 139917104916224,19
Timer 3: 139917122738048,20

以上程序中,启动了两个线程和三个定时器,定时器每秒触发一次,注意timer1,timer2与一个strand绑定,timer3未指定,结果就是每次timer1和timer2执行时都由一个线程执行,因此保证了它们的顺序性,timer3则可能与timer1/timer2同线程,也可能不同,理论上就不能保证timer3一定发生在timer1/time2之后。另外timer1/timer2执行的线程也不是固定的(顺序一定要保证),也说明我们无法控制io_context对任务的分配。

3. Thread Pool

Boost.Asio中提供了一个简单的线程池,能够简化多线程的创建和任务的分配,过程如下:

  • 创建线程池 thread_pool(n)
  • 添加任务  post([](){}),如果有多个任务,就多次post
  • 接入主线程   pool.join()

虽然thread_pool和io_context的基类相同execute_context,但它们似乎不相关,其实将thread_pool引入io_context的多线程编程就好了。

四、缓存块

Boost.Asio库中出现数据缓存块的概念,很大原因在于事件驱动的编程方式,在这种方式下,发送/写时,将数据放入缓存区,交给系统处理,侦听/读时,告诉系统这有一个区域,有数据时放入,因此数据缓存块是系统与程序交互的边界,而且因为需要跨函数,这一缓存块还要是“全局可见”的。

一般情况下,直接使用的缓存区类有三个:

const_buffer:只可读的缓存块,注意 const_buffer本身并不“拥有”内存区,在使用时需注意引用的内存区是否有效。

mutable_buffer:可修改的缓存块,与const_buffer一样,mutable_buffer本身并不“拥有”内存区,在使用时需注意引用的内存区是否有效。

streambuf:基于std::streambuf的字节流缓存块,长度是可增长的,但有一最大值限制,目前Asio采用单一连续的内存块来实现,streambuf与前两个类是不同的,它直接拥有内存区,但由于采用了同一块内存供读写,因此要注意读写同时进行时的操作。

相关标签: C C Boost Asio