这一章我们将尝试建立通用的术语,针对Akka的目标:并发、分布式系统等,定义一个坚实的沟通基础。请注意,很多这些术语都没有公认的定义。我们只是希望在Akka文档的范围内给出可用的定义。
并发和并行是相关的概念,但有一些小差异。并发 指的是两个或多个任务都正在开展,即使他们没有被同时执行。可以用时间切片实现这样的例子:时间片部分任务顺序执行和混入其它任务交叉执行。并行 则是指可以真正同步执行。
一个方法调用是 同步,调用者不能继续处理,直到该方法返回一个值或抛出一个异常。另一方面,一个 异步 调用允许调用者先调用后面有限的几个步骤,并且当该方法完成时,额外的机制可以通知(也许是一个注册的回调callback,一个Future或一个消息)。
一个同步的API也许会使用阻塞实现同步性,但这不是必要的。一个CPU非常密集的任务可能会导致类似阻塞的行为。通常推荐使用异步API,因为它们确保系统能够继续处理。Actor原生就是异步的:一个Actor可以在发送消息后继续处理,而不需要等待消息确实被送达。
如果一个线程的延迟会导致其它一些线程无限期的延迟,我们称之为 阻塞。一个很好的例子是资源可以被线程通过互斥锁独占使用。如果这个线程无限期地占有这个资源(例如不小心进入死循环),其他等待这个资源的线程就无法处理了。相比之下,非阻塞 意味着没有线程可以无限期的阻塞其他线程。
非阻塞是首选的。当包含一个阻塞操作时,作为整个系统的处理难以全面保证。
当多个参与者都在互相等待彼此达到某个特殊的状态才能继续处理的时候,死锁 出现了。因为其它人都不达到特定状态,他们中没有人可以继续处理(就像《第二十二条军规》描述的那样),所有相关子系统都停顿了。死锁和阻塞紧密相关,因为阻塞使得一个参与者线程可以无限期地推迟其他线程的处理。
在死锁中,没有参与者可以继续处理,然而相对的 饥饿 发生,当有些参与者可以不断地处理,但有另一个或多个可能不行。一个典型的场景是一个幼稚的排程算法——总是选择高优先级任务优先于低优先级的任务。如果传入的高优先级任务的数量一直足够多,则低优先级的任务永远不会被完成。
活锁 和死锁类似,没有参与者可以处理。区别在于与进程进入等待其他进程处理的“冻结”状态不同,参与者不断地改变他们的状态。一个示例场景是两个参与者拥有两个同等的可用资源。他们分别试图获取资源,并且检查是不是另一个参与者也需要这个资源。如果该资源被另一个参与者请求,则它们试图获取其它资源实例。在不幸的情况下,也许两个参与者会不停的在两个资源上“反弹”,永远在谦让从而无法获得资源。
假设一组事件的排序可能受到外部非确定性的影响,我们称之为 竞态条件。竞态条件经常在多个线程有一个共享可变状态时出现,一个线程对这个状态的操作可能被交织从而导致意外的行为。虽然这是常见的情况,但是共享状态并不一定会导致竞态条件。例如一个客户端发送无序的包(例如UDP数据包)P1
,P2
到服务器。包可能经过不同的网络路由器传送,所以服务器可能先收到P2
,后收到P1
。如果消息中没有包含发送顺序的相关信息的话,服务器是不可能确定包是否是按照发送顺序接收的。根据包的内容这可能会导致竞态条件。
注意
对两个actor之间的消息发送,Akka唯一提供的保证是消息的发送顺序是被保留的。详见 消息传送可靠性
正如前面章节论述的,有几个原因使得阻塞是不可取的,包括危险的死锁和降低系统的吞吐量。下面的章节我们将从不同深度讨论各种非阻塞特性。
如果一个方法的调用可以保证在有限步骤内完成,则称该方法是 无等待 的。如果方法是 有界无等待 的,则方法的执行步数有一个确定的上界。
从这个定义可以得出无等待的方法永远不会阻塞,因此死锁是不可能发生的。此外,因为每个参与者都可以经过有限步聚后继续执行(当调用完成),所以无等待方法也不会出现饥饿的情况。
无锁 是比 无等待 更弱的特性。在无锁调用的情况下,无限地经常有一些方法在有限步骤内完成。这个定义暗示着对无锁调用是不可能出现死锁的。另一方面, 部分方法调用 在有限步骤内 结束,不足以保证 所有调用最终完成。换句话说,无锁不足以保证不会出现饥饿。
无阻碍 是这里讨论的最弱的无阻塞保证。对一个方法,当在某一个它独自执行的时间点(其他线程不在执行,例如都挂起了),之后它在有限步后能够结束,我们称之为 无阻碍。所有无锁的对象都是无阻碍的,但反之一般不成立。
乐观并发控制(OCC) 方法通常是无阻碍的。OCC的做法是每一位参与者都试图在共享对象上执行操作,但是如果参与者检测到来自其他参与者的冲突,它回滚修改,并根据排程再次尝试。如果在某一个时间点,其中一个参与者,是唯一一个尝试修改的,则其操作就会成功。