在之前的文章中,我们有介绍如何推导一个简单的任务队列。
本文介绍如何利用Boost.Asio构建不需要显示地加锁或同步的线程池。
Boost.Asio 有两种支持多线程的方式:
- 在多线程的场景下,每个线程都持有一个
io_service
,并且每个线程都调用各自的io_service
的run()
方法。 - 全局只分配一个
io_service
,并且让这个io_service
在多个线程之间共享,每个线程都调用全局的io_service
的run()
方法。
每个线程一个 I/O Service
让我们先分析第一种方案:在多线程的场景下,每个线程都持有一个io_service
(通常的做法是,让线程数和 CPU 核心数保持一致)。那么这种方案有什么特点呢?
- 在多核的机器上,这种方案可以充分利用多个 CPU 核心。
- 某个 socket 描述符并不会在多个线程之间共享,所以不需要引入同步机制。
- 在 event handler 中不能执行阻塞的操作,否则将会阻塞掉
io_service
所在的线程。
下面我们实现了一个AsioIOServicePool
,封装了线程池的创建操作:
1 | class AsioIOServicePool |
AsioIOServicePool
使用起来也很简单:
1 | std::mutex mtx; // protect std::cout |
一个 I/O Service 与多个线程
另一种方案则是先分配一个全局io_service
,然后开启多个线程,每个线程都调用这个io_service
的run()
方法。这样,当某个异步事件完成时,io_service
就会将相应的 event handler 交给任意一个线程去执行。
然而这种方案在实际使用中,需要注意一些问题:
- 在 event handler 中允许执行阻塞的操作 (例如数据库查询操作)。
- 线程数可以大于 CPU 核心数,譬如说,如果需要在 event handler 中执行阻塞的操作,为了提高程序的响应速度,这时就需要提高线程的数目。
- 由于多个线程同时运行事件循环(event loop),所以会导致一个问题:即一个 socket 描述符可能会在多个线程之间共享,容易出现竞态条件(race condition)。譬如说,如果某个 socket 的可读事件很快发生了两次,那么就会出现两个线程同时读同一个 socket 的问题 (可以使用strand解决这个问题)。
值得一提的还有,成员变量 work_guard_
的作用是让 io_context
即使在没有异步任务可执行时也保持运行(即 io_context::run
不返回)。详见 Stack Overflow 的讨论:Why should I use io_service::work?
1 |
|
输出(每次都不一样):
1 | Hello(0) |