要理解这个答案,您必须从操作系统层面开始考虑事件并发。首先,您需要从线程开始,线程是操作系统可以运行的最小代码部分,并最终处理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 生产者和消费者,因为写入/发送方必须等待读取/接收方,反之亦然。当任一进程正在等待时,它们明确地将执行权让给其他进程。当协程 yield 时,它的作用域程序状态被保存在堆栈帧上,因此可以避免在事件循环中管理多层 baton 状态所带来的困惑。
使用构建在这些事件通道上的应用程序,我们可以构建任意可重用的并发逻辑,算法不再像意大利面条代码一样混乱。在纯 CSP 系统中,如果您向通道写入数据而没有读取者,则会被阻塞。通道端点在程序内部通过句柄知道。
演员系统在几个方面与其他系统不同。 首先,端点是演员线程,并且它们被命名并在主程序之外已知。 第二个区别是这些通道上的发送和接收是缓冲的。 换句话说,如果您向一个演员发送消息,但没有一个正在侦听或忙碌,您不会被阻塞,直到有一个从其输入通道读取为止。 还存在其他差异,例如一个演员可以同时发布到两个不同的演员。
正如您可能猜到的那样,演员系统可以很容易地从CSP系统构建。 还有其他细节,例如等待特定事件模式并从中进行选择,但这是基础知识。
我希望这能够使事情更加清晰。
可以从这些思想中构建其他结构。 各种编程系统(Go,Erlang等)在其中包括了CSP实现。 Inferno和Node9等操作系统使用CSP和通道作为其分布式计算模型的基础。
Go语言:
https://zh.wikipedia.org/wiki/Go
Erlang语言:
https://zh.wikipedia.org/wiki/Erlang
Inferno操作系统:
https://zh.wikipedia.org/wiki/Inferno_(%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F)
Node9:
https://github.com/jvburnes/node9