我开始学习 Scala 和函数式编程。我正在读这本书!Programming scala: Tackle Multi-Core Complexity on the Java Virtual Machine”。在第一章中,我看到了 Event-Driven concurrency 和 Actor 模型这个词。在我继续阅读这本书之前,我想要一个关于事件驱动并发或 Actor 模型的想法。
什么是事件驱动并发,它与 Actor Model 有什么关系?
我开始学习 Scala 和函数式编程。我正在读这本书!Programming scala: Tackle Multi-Core Complexity on the Java Virtual Machine”。在第一章中,我看到了 Event-Driven concurrency 和 Actor 模型这个词。在我继续阅读这本书之前,我想要一个关于事件驱动并发或 Actor 模型的想法。
什么是事件驱动并发,它与 Actor Model 有什么关系?
事件驱动编程模型涉及注册代码以在给定事件触发时运行。一个例子是,而不是调用从数据库返回一些数据的方法:
val user = db.getUser(1)
println(user.name)
您可以改为注册一个回调以在数据准备好时运行:
db.getUser(1, u => println(u.name))
在第一个示例中,没有发生并发;当前线程将阻塞,直到db.getUser(1)
从数据库返回数据。在第二个示例db.getUser
中将立即返回并继续执行程序中的下一个代码。与此同时,回调u => println(u.name)
将在未来的某个时间点执行。
有些人更喜欢第二种方法,因为这并不意味着需要大量内存的线程无谓地等待缓慢的 I/O 返回。
Actor 模型是事件驱动概念如何用于帮助程序员轻松编写并发程序的一个示例。
从超高层次来看,Actor 是定义一系列事件驱动消息处理程序的对象,这些处理程序在 Actor 接收到消息时被触发。在 Akka 中,Actor 的每个实例都是单线程的,但是当这些 Actor 中的许多放在一起时,它们会创建一个具有并发性的系统。
例如,Actor可以并行A
向 Actor 发送消息。Actor并且可以将消息返回给 Actor 。Actor将具有消息处理程序来接收这些消息并按需要运行。B
C
B
C
A
A
要了解有关 Actor 模型的更多信息,我建议您阅读 Akka 文档。它写得很好:http : //doc.akka.io/docs/akka/2.1.4/
网络上也有很多关于事件驱动并发的优秀文档,我们比我在这里写的要详细得多。http://berb.github.io/diploma-thesis/original/055_events.html
Theon 的回答提供了一个很好的现代概述。我想补充一些历史观点。
Tony Hoare 和 Robert Milner 都开发了用于分析并发系统的数学代数(Communicating Sequential Processes,CSP 和 Communicating Concurrent Systems,CCS)。对我们大多数人来说,这两个看起来都像是繁重的数学,但实际应用相对简单。CSP 直接导致了 Occam 编程语言等,而 Go 是最新的例子。CCS 导致了 Pi 演算和通信通道端的移动性,这是 Go 的一部分,在过去十年左右被添加到 Occam 中。
CSP 纯粹通过考虑仅通过事件交换进行交互的自治实体(“进程”,v.轻量级事物,如绿色线程)来建模并发。传递事件的媒介是沿着渠道。流程可能必须处理多个输入或输出,它们通过选择首先准备好的事件来完成此操作。事件通常将数据从发送方传送到接收方。
CSP 模型的一个主要特征是一对进程只有在两者都准备好时才进行通信——实际上,这导致了通常称为“同步”通信的情况。然而,实际的实现(Go、Occam、Akka)允许对通道进行缓冲(Akka 中的正常状态),因此事件的锁步交换通常实际上是解耦的。
所以总而言之,一个基于事件驱动的 CSP 系统实际上是一个由通道连接的进程的数据流网络。
除了事件驱动的 CSP 解释之外,还有其他的。一个重要的例子是“事件轮”方法,曾经流行用于建模并发系统,但实际上只有一个处理线程。这样的系统通过将事件放入处理队列并在适当的时候处理它们,通常通过回调来处理它们。Java Swing 的事件处理引擎就是一个很好的例子。还有其他的,例如基于时间的模拟引擎。人们可能会认为 Javascript / NodeJS 模型也适合这一类。
所以总而言之,事件轮是一种表达并发但没有并行性的方式。
具有讽刺意味的是,我上面描述的两种方法都被描述为事件驱动,但它们所指的事件驱动在每种情况下都是不同的。在一种情况下,类似硬件的实体被连接在一起;另一方面,几乎所有动作都由回调执行。CSP 方法声称是可扩展的,因为它是完全可组合的;它自然也擅长并行执行。如果有任何理由偏爱其中一个,那么可能就是这些了。
要理解这个问题的答案,您必须从操作系统层向上查看事件并发。首先,您从线程开始,线程是操作系统可以运行的最小代码部分,最终处理 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 系统可以很容易地从 CSP 系统构建。还有其他细节,例如等待特定事件模式并从中进行选择,但这是基础。
我希望这能澄清一点。
可以从这些想法构建其他结构。各种编程系统(Go、Erlang 等)在其中包含 CSP 实现。Inferno 和 Node9 等操作系统使用 CSP 和 Channels 作为其分布式计算模型的基础。
Go: https://en.wikipedia.org/wiki/Go_(programming_language)
Erlang: https://en.wikipedia.org/wiki/Erlang_(programming_language)
Inferno: https://en.wikipedia.org/wiki/ Inferno_(operating_system)
Node9:https ://github.com/jvburnes/node9