我的问题是在阅读 Martin Kleppmann 所著的《高负载应用程序:编程、扩展、支持》ISBN 978-5-4461-0512-0 的书时出现的,我在第 383 页遇到了一个我不清楚的点。图 9.4
在本页的前半部分,作者讨论了阴影矩形,并表示该操作是非线性的。我不明白为什么?
直到第383页,作者解释了术语“线性化操作”的含义。他通过给出时间上重叠的读写操作的示例来解释这一点:
- 写操作收到成功确认后,从那一刻起,所有读操作都会收到一个新的
- 作者还提到,在写操作期间,有一个时刻,旧值被自动更改为新值,并且发起写操作的客户端尚未收到确认,而有读请求的客户端已经收到了新值
回到第383页的图9.4,我不明白这一点,为什么作者特别关注客户B?毕竟,在此之前,有一个时刻,客户端 C 使用 操作将 2 更改为 4 cas(x,2,4)
,导致客户端 A 读取了新值 4。客户端 A 和客户端 B 一样,也是竞争执行的!为什么作者不说客户端A,却说客户端B,和客户端A一样的情况,突然说他的读操作是非线性的。
作者的话“在没有其他查询的情况下,如果 read B 返回 2 就好了”完全令人困惑!为什么好呢?
书中的图 9.4 定义了可串行化与线性化的区别。正是这种认识才值得强调。
TL;DR 线性化点是操作中的某个时刻(即使是原子操作),此后变量的新值对系统的其余部分可见。如果这个新值对于某些单独的资源(线程/服务)不可见(但对其其他人可见),则系统是非线性的。
解释
互联网上充满了这些术语的科学定义;我将尝试用简单的语言解释它们,省略细节。
任何函数(即使它是原子的)都是一组特定的操作。
我们以CAS(比较和交换)函数为例。一方面,它被认为是原子的,但它执行 3 个操作:读取该地址处的值,将其与预期值进行比较,然后:如果预期值与读取的值匹配,则覆盖该地址处的值。对于现代处理器来说,这通常是一条指令(我们不会详细介绍)并且它是真正的原子指令(就像立即执行一样)。
唯一的是,这个操作,实时来说,不会是瞬间完成,而是需要一段时间。毕竟,您需要准备调用堆栈,从内存加载数据,甚至直到电信号通过电路板。所有这些漫长的准备过程需要花费大量时间,但录制操作本身执行得相当快。但事实证明,CAS 在时间上不能被表示为一个整体,而是可以持续相当长一段时间的东西。
现在假设我们有 3 个处理程序,第一个处理程序尝试使用 CAS 执行写入,只是这个处理程序非常慢并且 CAS 操作(为了便于理解)在 1 秒(1000 毫秒)内完成。但是新值(假设我们清楚地知道这一点)可以在 555ms 后读取,但其余时间需要用于官方目的(准备、清理、记录某些内容)。另外两个正在尝试读取信息(假设读取在 100ms 内完成)。它们都是并行工作的。
可串行性是系统执行操作的能力,就好像它们是某种事务一样。那些。严格地一个接一个地执行操作,就像它们在同一个线程上执行一样。那么,本质上,这样的问题就不会存在;我们可以明确地放置:
写入(1000 毫秒)- 读取(100 毫秒)- 读取(100 毫秒)
但形式上编译器可以将其排列为:
读取(100 毫秒)- 写入(1000 毫秒)- 读取(100 毫秒)
与事务一样,我们执行操作,但提交仅在我们完全完成操作后才会发生。
但这里事实证明我们已经同步了我们的系统,并且需要 1200 毫秒才能完成(这是一个很长的时间)。
线性化是可串行化的一个宽松的概念。我们知道操作不是瞬间执行的,而是实时存在某个点(时刻),系统开始看到新的值。但操作本身可能尚未完成。这将是线性化点。
具体看图:
客户 C 进行输入
cas(x, 2, 4)
。有用。此时,客户端A执行
read(x)
。恰好操作的线性化点(当数据可见时)read(x)
是在它工作之后cas(x, 2, 4)
,所以它已经收到了值read(x) = 4
。接下来,
read(x)
客户端 A 读取后,客户端 B 也读取到相同的read(x)
,其线性化点位于 cas 之后,客户端 A 读取之后。但是,由于某种原因,客户端 B 收到了read(x) = 2
,但我们清楚地知道read(x)
客户端 B 发生在read(x)
客户端 A 之后。所以客户端 B 也应该具有值read(x) = 4
,但由于某种原因客户端 B 的信息不正确(可能 x 来自客户端 B 的缓存,因为它之前做了记录,或者可能是其他原因)发生了),但最终我们得到了我们得到的,这意味着在整个系统中有一个时刻,读取不是来自每个人看到的实际数据,而是来自一些本地数据。