概念
多线程编程中, 我们常常把任务分解成离散的工作单元(每个工作单元也许很小), 以期并行处理. 但是, 为每个工作单元创建线程(比如boost::async), 尤其是大量创建, 会存在一些不足:
- 线程生命周期的开销非常高. 线程的创建和销毁都是需要时间的.
- 资源消耗. 活跃的线程会消耗系统资源, 尤其是内存. 根据平台不同, 可创建线程的数量也是有限的.
- 频繁的资源竞争和上下文切换, 降低CPU的使用效率. 所以, 工作单元小而多的时候, 我们并不希望总是创建新线程. 似乎我们需要某种机制来控制什么线程执行什么工作单元. 这就是我们说的Executor框架, 它抽象了任务的执行策略.
这个策略可能是多种多样的, 也许是线程池, 也许是为每个单元创建新线程, 也许我们就希望单线程串行执行…
通过模板(或者接口), 我们可以灵活地指定executor, 或者为不同性质的任务指定不同的executor.
实际上, 根据不同的线程数(number of execution contexts), 不同的任务排序策略(how they are prioritized), 不同的选择策略(how they are selected), executor分为几大类, 好多种[1]:
-
线程池(Thread Pools)
- simple unbounded thread pool: 将工作单元放到任务队列中, 然后维护一堆线程, 每个线程去任务队列取工作单元, 然后执行, 如此往复.
- bounded thread pool: 跟无界线程池很类似, 但是它的任务队列是有界的, 这限制了线程是中排队的工作单元的数量.
- thread-spawning executor: 总是为新任务创建新线程.
- prioritized thread pool: 任务队列是个优先队列.
- work stealing thread pool: 线程池本身有个主任务队列, 每个工作线程也维护了自己的任务队列. 当工作线程自己的任务队列没有任务时, 就会去主任务队列取任务或者别的工作线程那”偷”任务. 适用于任务比较小的情况, 可以避免在主任务队列上的频繁竞争.
- fork-join thread pool: 允许在任务中继续(递归地)分解(fork)并提交任务, 提交后进入等待时, 不是干等, 而是执行所在工作线程的任务队列的任务或者”偷”个任务回来执行. 等子任务完成后, 合并(join)得到任务自身的结果. 通常基于work stealing thread pool实现, 比如Java的ForkJoin框架.
-
互斥执行(Mutual exclusion executors)
- serial executor: 串行地执行, 也许在另一个线程, 但任务间是不会并发的, 所以不需要额外的互斥.
- loop executor: 跟serial executor类似, 但是执行的线程不是executor创建的, 而是别的调用者”给(donate)”的. 常用于测试.
- GUI thread executor: boost说的, 我也不知道什么意思.
-
Inline Executor: submit的时候就把任务执行了(在提交者的线程), 故不需要队列, 也不起线程. 常用于任务很小, 没必要放别的线程执行, 或者出于性能考虑, 直接执行比较好, 但接口非得executor的情况.
boost就列了这么多, 事实上我们还能列出好多来(比如folly, java.util.concurrent). 不过本文并不打算全部一次讲清楚我没这么厉害, 而是讲boost已经有的basic_thread_pool, serial_executor, loop_executor, inline_executor, 以及thread_executor(thread-spwaning executor).
work stealing 和 fork-join我们会分别单列一篇的讨论.