在这篇文章中,我们先介绍一下Executor模式,然后试着用Asio实现一个优先队列。
Executors
Executors
定义了一组关于何时何地如何执行函数对象的一组规则。在多线程序中,可以使用它将任务的提交和执行解耦。
举例来说:
Type of executor | Where, when and how |
---|---|
操作系统 | 进程中的任意线程 |
Thread pool | 仅限于线程池中的线程 |
Strand | 按照FIFO的顺序依次执行队列中的函数对象 |
Future / Promise | 任意线程。函数对象抛出的异常保存在promise中 |
满足一定规则的类都可以被称为Executors
,所以Executors
不仅指上述这些类别。就像标准库里的allocators一样,用户可以根据自己的规则定义自己的executor
。
一般来说,executor
提供了两种基本的提交任务的方式:dispatch和post。它们的区别在于任务调度的优先程度。
dispatch
dispatch操作的优先程度最高,它表示在满足executor
条件的情况下尽快地执行函数对象。
1 | void f1() |
通过对ex
调用一个dispatch操作,我们向executor
发起了立刻执行指定任务的一个请求。当然,任务是否立刻执行依赖于executor
具体的规则:
Type of executor | Behaviour of dispatch |
---|---|
操作系统 | 在从dispatch() 返回前就调用函数对象 |
线程池 | 如果我们在线程池内部调用该方法,线程池会在从dispatch() 返回前就调用函数对象。否则的话会将任务加入到线程池的任务队列中(稍后执行) |
Strand | 如果我们在Strand内部调用该方法,或者Strand内的任务队列为空, Strand会在从 dispatch() 返回前就调用函数对象。否则的话会将任务加入到Strand的任务队列中(稍后执行) |
Future / Promise | 将函数对象包裹在try/catch中,并且在从dispatch() 返回前就调用它 |
这种方式的另一个好处是如果允许的话编译器会折叠(inline)函数调用。
post
post操作则会将任务排进处理队列,然后返回,任务会在稍后某个时机被处理。
1 | post(ex, f1); |
任务的执行方式取决于executor
具体的规则:
Type of executor | Behaviour of post |
---|---|
操作系统 | 将函数对象加入到系统的线程池任务队列中 |
线程池 | 将函数对象加入到线程池的任务队列中 |
Strand | 将函数对象加入到Strand的任务队列中 |
Future / Promise | 将函数对象包裹在try/catch中,并加入到系统的任务队列中 |
example
我们写一个向线程池post任务的小例子:
1 | class bank_account |
这里,线程池就是一个典型的execution context,它表示了函数对象执行的位置。类似的还有Boost::Asio::io_context
。
Priority Queue
仿照Boost::Asio::io_context
的实现,我们可以定义一个priority_handler_queue
类,它继承于boost::asio::execution_context
,内部有一个executor
满足Executor的要求,允许用户提交任务。
1 | class priority_handler_queue : public boost::asio::execution_context |
同时,它也有一个可以多线程运行的run()
方法和stop()
方法:
1 | void run() |
基于标准库的工具,我们可以实现一个能够按照优先级对提交的函数对象进行保存的队列:
1 | using handler_ptr = typename std::shared_ptr<queued_handler_base>; |
测试
一个典型的测试场景如下:
1 |
|
附录:源码
Executors_and_Asynchronous_Operations_Slides.pdf