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
,封装了线程池的创建操作 [完整代码]:
AsioIOServicePool
使用起来也很简单:
一个 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
解决这个问题)。
下面实现了一个线程池,在每个 worker 线程中执行io_service
的run()
方法 [完整代码]:
无锁的同步方式
要怎样解决前面提到的竞态条件呢?Boost.Asio 提供了io_service::strand
:如果多个 event handler 通过同一个 strand 对象分发 (dispatch),那么这些 event handler 就会保证顺序地执行。
例如,下面的例子使用 strand,所以不需要使用互斥锁保证同步了 [完整代码]:
多线程 Echo Server
下面的EchoServer
可以在多线程中使用,它使用asio::strand
来解决前面提到的竞态问题 [完整代码]: