在C++背景下,执行者模式是什么?

8

asio的作者Christopher Kohlhoff正在为C++编写执行器库和提案。他迄今为止的工作包括这个repo文档。不幸的是,解释部分尚未编写。到目前为止,文档提供了一些库功能的示例,但我觉得我没有错过什么。这似乎比一系列花哨的调用函数更重要。

我在Google上找到的所有内容都非常与Java相关,并且其中很多内容特定于特定的框架,因此我很难弄清楚这个“执行器模式”的含义。

在这种情况下,执行器是什么?它们有什么作用?它们何时会有帮助?执行器中存在哪些变化?执行器的替代方案是什么,它们如何比较?特别是,在事件循环中似乎有很多重叠,其中事件是初始输入事件、执行事件和关闭事件。

当尝试理解新的抽象时,我通常发现理解动机是关键。因此对于执行器,我们试图抽象什么以及为什么?我们试图使什么通用?如果没有执行器,我们需要做哪些额外的工作?


1
使用重载的调用operator()(... args)运算符? - πάντα ῥεῖ
@πάνταῥεῖ 我不理解你的评论。 - Praxeolitic
那就是表达执行器/函数对象模式的惯用方式。 - πάντα ῥεῖ
我认为这与这篇论文所说的类似:并行算法需要执行器| N4406 - user2486888
@Praxeolitic 你是指类似于“模板函数”模式吗?相关的是“策略模式”。 - πάντα ῥεῖ
显示剩余5条评论
1个回答

6
执行者最基本的好处是将程序并行性的定义与使用分开。Java执行器模型存在的原因是,大体上来说,在您编写代码时,您实际上不知道哪种并行性模型最适合您的情况。您可能从并行性中获益甚少,根本不需要使用线程,您可能最好为每个核心运行一个长时间运行的专用工作线程,或根据当前负载动态缩放线程池,在它们空闲一段时间后清除线程以减少内存使用、上下文切换等,或者只需在需要时启动一个线程,任务完成后退出。
关键在于当您第一次编写代码时,几乎不可能知道哪种方法最好。您可能知道并行性可以帮助您的地方,但是在传统的线程中,您最终会交错并行性 "配置"(何时以及是否创建线程)和并行性的使用(确定使用什么参数调用哪些函数)。当您像这样混合代码时,要对不同选项进行性能测试非常麻烦,因为每个线程启动都是独立的,并且必须单独更新。
执行器模型的主要优点是并行性配置在 一个 地方完成(即创建执行器的地方),而那些执行器的 用户 不需要知道任何关于它的信息。他们只需向执行器提交工作,接收一个 future,并在以后的某个时候从 future 中检索结果(必要时进行阻塞)。如果您想尝试其他配置,只需更改定义执行器的一行代码并再次运行代码即可。即使您决定需要为您的代码的不同部分使用不同的并行性模型,添加第二个执行器并更改第一个执行器的一些用户以使用第二个执行器相比手动重写每个站点的线程详细信息更容易;只要执行器的名称是(相对)唯一的,查找用户并更改它们以使用不同的执行器就很容易。执行者简化了您的代码(通过避免将线程创建/管理与线程执行的任务混合在一起),简化了性能测试。
作为副产品,还可以抽象出将数据传输到和从工作线程中的复杂性(submit 方法封装前者,future 的 result 方法封装后者)。std::async 使您获得其中一些好处,但没有真正控制所涉及的并行性(仅是强制线程、强制当前线程中延迟执行或让编译器/库决定是否在可能情况下使用线程池,且无法精细地控制线程池的使用方式)。真正的执行器框架提供了 std::async 未能提供的控制,且同样易于使用。

这是否与 .NET 的 TaskScheduler 相对应?或者任务和任务计划程序如何表示职责的分离:任务是工作单元,而计划程序决定将其安排到专用线程、线程池中的某个线程或其他上下文(可能与 UI 框架绑定)。 - Tanveer Badar
1
@TanveerBadar:是的,看起来填补了类似的空缺。.NET的方法似乎试图鼓励使用TaskFactory逐个创建任务,而Java的方法则接受一个特定的接口(Callable<T>),该接口通过构造函数绑定参数并通过接口方法返回结果,但思想大致相同。由于具有lambda和可变模板,C++实际上可能更容易使用(当您可以直接传递函数引用后跟参数时,无需使用TaskFactory或实现Callable),但思想是相同的。 - ShadowRanger
感谢您的回答和进一步的解释。我可以放心地说,今天我学到了新东西。 - Tanveer Badar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接