要了解这个问题的答案,您必须从操作系统层向上查看事件并发性。首先,您从线程开始,线程是操作系统可以运行的最小代码部分,最终处理 I/O、计时和其他类型的事件。
操作系统将线程分组到一个进程中,在这些进程中它们共享相同的内存、保护和安全权限。在该层之上,您有用户程序,这些程序通常发出由用户库处理的 I/O 请求。
I/O 库以两种方式之一处理这些请求。类 Unix 系统使用“反应器”模型,其中库为系统中所有不同类型的 I/O 和事件注册 I/O 处理程序。当特定设备上的 I/O 准备就绪时,这些处理程序被激活。类似 Windows 的系统使用 I/O 完成模型,其中发出 I/O 请求并在请求完成时触发回调。
如果您要直接使用这两种模型,则需要大量开销来管理整个程序状态。但是,如果您直接使用事件模型,则某些编程任务(Web 应用程序/服务)有助于实现看似更直接的实现,但您仍然需要管理所有这些程序状态。为了跨多个相关事件的调度跟踪程序逻辑,您必须手动跟踪状态并将其传递给回调。这种跟踪结构通常称为状态上下文或指挥棒。正如您可能想象的那样,将警棍到处传递给许多看似无关的处理程序会导致一些极其难以阅读和意大利面条式的代码。编写和调试也很痛苦——尤其是当您尝试处理各种并发执行路径的同步时。你开始进入 Futures,然后代码变得非常难以阅读。
一个著名的事件处理库是调用 libuv。它是一个可移植的事件循环,将 Unix 的反应器模型与 Windows 的完成模型集成到一个通常称为“proactor”的模型中。它是驱动 NodeJS 的事件处理程序。
这使我们能够交流顺序流程。
https://en.wikipedia.org/wiki/Communicating_sequential_processes
我们没有使用一种或多种并发模型(以及它们经常相互竞争的约定)来编写异步 I/O 调度和同步代码,而是将问题抛在脑后。我们使用一个看起来像普通顺序代码的“协程”。
一个简单的例子是协程通过事件通道从另一个发送单个字节的协程接收单个字节。这有效地同步了 I/O 生产者和消费者,因为写入者/发送者必须等待读取者/接收者,反之亦然。当任何一个进程都在等待时,它们会明确地将执行权交给其他进程。当协程产生时,其范围内的程序状态会保存在堆栈帧中,从而使您免于在事件循环中管理多层接力棒状态的混乱。
使用基于这些事件通道构建的应用程序,我们可以构建任意、可重用的并发逻辑,并且算法不再像意大利面条代码。在纯 CSP 系统中,如果您写入通道并且没有阅读器,您将被阻止。通道端点通过程序内部的句柄是已知的。
Actor 系统在几个方面有所不同。首先,端点是参与者线程,它们在主线程序外部被命名和知道。第二个区别是这些通道上的发送和接收是缓冲的。换句话说,如果您向演员发送消息并且没有人在收听或忙碌,那么您不会被阻止,直到有人从他们的输入通道中读取。还存在其他差异,例如一个演员可以同时发布给两个不同的演员。
正如您可能猜到的,Actor 系统可以很容易地从 CSP 系统构建。还有其他细节,例如等待特定事件模式并从中进行选择,但这是基础。
我希望能澄清一点。
可以根据这些想法构建其他结构。各种编程系统(Go、Erlang 等)在其中包含 CSP 实现。 Inferno 和 Node9 等操作系统使用 CSP 和 Channels 作为其分布式计算模型的基础。
去:https://en.wikipedia.org/wiki/Go_(programming_language)
二郎:https://en.wikipedia.org/wiki/Erlang_(programming_language)
地狱:https://en.wikipedia.org/wiki/Inferno_(operating_system)
节点9:https://github.com/jvburnes/node9