什么是事件驱动并发?

8
我开始学习Scala和函数式编程。我正在阅读《Programming scala: Tackle Multi-Core Complexity on the Java Virtual Machine》这本书的第一章,看到了“事件驱动并发”和“Actor模型”这个词。在继续阅读本书之前,我想了解一下“事件驱动并发”或“Actor模型”的概念。
什么是“事件驱动并发”,它与“Actor模型”有什么关系?

2
你已经做了/阅读了什么来了解事件驱动并发是什么?Google至少应该能帮助你。对我来说,前三个搜索结果分别是:http://berb.github.io/diploma-thesis/original/055_events.html,https://www.youtube.com/watch?v=2gcrTsQ7yi4,https://en.wikipedia.org/wiki/Event-driven_programming。 - Malte Schwerhoff
3个回答

17
一个事件驱动的编程模型涉及将代码注册到在特定事件触发时运行。例如,与其调用从数据库返回某些数据的方法:
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模型是事件驱动概念如何帮助程序员轻松编写并发程序的一个例子。
从超高层面来看,演员是定义一系列事件驱动消息处理程序的对象,当演员接收到消息时,这些处理程序被触发。在Akka中,每个Actor实例都是单线程的,但是当许多这些Actors组合在一起时,它们创建了一个具有并发性的系统。
例如,Actor A可以并行发送消息到Actor BC。Actor BC可以向Actor A发送消息。Actor A将具有接收这些消息并按预期方式行事的消息处理程序。
要了解更多关于Actor模型的信息,我建议阅读Akka文档。它非常好写:http://doc.akka.io/docs/akka/2.1.4/ 还有很多关于事件驱动并发的良好文档可以在网络上找到,比我在这里写的详细得多。http://berb.github.io/diploma-thesis/original/055_events.html

3
Theon提供了一个很好的现代概述,我想增加一些历史背景。Tony Hoare和Robert Milner都开发了用于分析并发系统的数学代数(通信顺序处理,CSP和通信并发系统,CCS)。对于我们大多数人来说,这两个都看起来像是沉重的数学问题,但实际应用相对简单。CSP直接导致了Occam编程语言等其他编程语言,而Go则是最新的例子。CCS导致了Pi演算和通信通道端口的移动,这是Go的一部分,并在最近十年左右被添加到Occam中。
CSP仅通过考虑自主实体('processes',轻量级东西,如绿色线程)之间的事件交换来模拟并发。传递事件的介质沿着通道。进程可能必须处理多个输入或输出,它们通过选择准备就绪的事件来完成此操作。事件通常携带来自发送方到接收方的数据。
CSP模型的一个原则性特征是一对进程仅在两者都准备就绪时才进行通信-在实际情况下,这通常导致所谓的'synchronous' 通信。然而,实际实现(Go,Occam,Akka)允许缓冲通道(Akka中的正常状态),以便事件的锁步交换实际上经常是解耦的。
因此,总之,基于事件驱动的CSP系统实际上是由通道连接的进程的数据流网络。
除了对事件驱动的CSP的解释外,还有其他解释。一个重要的例子是“事件轮”方法,曾经很流行用于模拟并发系统,同时实际上只具有一个处理线程。这种系统通过将事件放入处理队列并通过回调按顺序处理它们来处理事件,通常是处理时间模拟引擎等。Java Swing的事件处理引擎就是一个很好的例子。还有其他的解释,例如用于基于时间的模拟引擎。人们可能认为Javascript / NodeJS模型也适合此类别。
因此,总之,事件轮是一种表达并发但没有并行的方式。
具有讽刺意味的是,我所描述的这两个方法都被描述为事件驱动,但它们在每种情况下所指的事件驱动的含义是不同的。在一种情况下,类似于硬件的实体被连接在一起;在另一种情况下,几乎所有操作都由回调执行。CSP方法声称具有可扩展性,因为它是完全可组合的;它自然擅长并行执行。如果有任何理由支持其中一种而不是另一种,则可能是这些原因。

1
要理解这个答案,您必须从操作系统层面开始考虑事件并发。首先,您需要从线程开始,线程是操作系统可以运行的最小代码部分,并最终处理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

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