当前位置:首页>java>[译] 编程语言内存模型

[译] 编程语言内存模型

  • 2026-01-13 13:03:48
[译] 编程语言内存模型

原文: Programming Language Memory Models (Memory Models, Part 2) by Russ Cox[1],发表于 2021 年 7 月 6 日。


编程语言内存模型回答如下问题:并行程序可以依赖什么样的行为在其线程之间共享内存。例如,考虑如下 C 语言程序(经典的 Message Passing),其中变量的初始值默认为 0。

// Thread 1           // Thread 2x = 1;                while(done == 0) { /* loop */ }done = 1;             print(x);

这个程序试图用 x 从线程 1 传递一条消息到线程 2,用 done 作为消息被接收的信号。假设线程 1 与线程 2 运行在不同的处理器上,并且都终止了,程序能保证最终打印 1 吗?编程语言内存模型就能回答类似于这样的问题。

虽然不同编程语言在细节上有所区别,但上述问题的一些通用答案基本适用于常见的现代多线程语言,包括 C,C++,Go,Java,JavaScript,Rust 以及 Swift:

  • 首先,如果 x 和 done 是普通变量,线程 2 的循环可能永远不会停止。编译器的一种常见优化方法是在首次使用某个变量时将其加载到寄存器中,然后在后续访问中尽可能长时间地重复使用该寄存器。如果线程 2 在线程 1 执行前将 done 加载到某个寄存器,它可能会在整个循环里重复使用这个寄存器,而不会注意到线程 1 后续更改了 done 的值。
  • 其次,即使线程 2 的循环终止了,并且观测到 done == 1,它仍然可能打印 0。编译器可能会由启发式的优化 pass,甚至是生成目标代码时对哈希表或其他中间数据结构的遍历方式重新排列程序的读写操作顺序。例如在编译后的代码中,线程 1 可能会在 done 之后写入 x,线程 2 也可能会在循环之前读取 x

鉴于该程序存在如此严重的缺陷,一个显然的问题是如何修复它。

现代语言提供了原子变量(atomic variable)或原子操作(atomic operations)等特殊的功能,来让程序同步其线程(译者注:后文将 atomics 翻译成原子,表示原子变量与原子操作的统称)。如果我们将 done 声明为原子变量(或者用原子操作对其读写),就能保证该程序终止并打印 1。将 done 声明为原子变量还会造成很多影响:

  • 编译器为线程 1 生成的代码必须确保在写入 done 之前,写入 x 的操作已完成并对其他线程可见。
  • 编译器为线程 2 生成的代码必须在循环的每一次迭代中都重新从内存中读取 done 的值。
  • 编译器为线程 2 生成的代码必须在读取 done 之后读取 x
  • 编译后的目标代码必须保证不让硬件级优化重新引入上述的任何问题。

将 done 变成原子变量后,程序按照预期执行了,成功地将 x 的值从线程 1 传递到了线程 2。

在原程序中,编译器重新排序代码后,线程 1 对 x 的写操作与线程 2 对 x的读操作可能同时发生。这就是数据竞争(data race)。在修改后的程序中,原子变量 done 充当了对 x 访问的同步:上述两个操作不再可能同时发生,程序是无数据竞争(data-race-free)的。一般来说,现代语言保证无数据竞争的程序总是按照顺序一致(SC)的方式执行,就好像来自不同线程的操作可能任意交织进行,但永远不会重排序。看过本系列上一篇文章的读者应该熟悉,这其实就是硬件内存模型中的 DRF-SC,编程语言内存模型同样采用了该模型。

顺便一提,这些原子变量或原子操作,其实更准确地说应当称为「同步原子(synchronizing atomics)」。诚然,这些操作在数据库意义上是原子的:它们允许并发的读写,其行为等价于按照某种顺序顺序执行;在普通变量上会构成数据竞争的情况,使用原子时则不会构成竞争。然而,更为重要的是,这些原子还能对程序的其余部分起到同步作用,从而为消除非原子数据上的竞争提供一种手段。尽管如此,标准术语仍然简单地称其为「原子(atomic)」,本文也沿用这一说法。读者只需记住,除非特别说明,文中出现的「原子」应理解为「具有同步语义的原子」。

编程语言内存模型规定了程序员和编译器需要做什么的具体细节,充当了他们之间的契约。上述概括性特征几乎适用于所有现代语言,但直到最近才趋于一致:在 21 世纪初,语言间的差异要大得多。即使在今天,不同语言在一些二阶问题上仍然存在显著差异,例如:

  • 对原子变量本身的顺序能做出什么保证?
  • 一个变量能否同时被原子操作和非原子操作访问?
  • 除了原子之外,有无其他同步机制?
  • 是否存在不同步的原子操作?
  • 对于包含竞争的程序,是否能做出某些保证?

在一些铺垫之后,本文余下部分将探讨不同语言如何回答这些及相关问题,以及它们实现目标的路径。文章还着重介绍了一路走来的诸多失败尝试,以强调我们仍在不断学习哪些方法有效,哪些无效。

硬件,Litmus Tests,Happens Before,以及 DRF-SC

在我们深入某个具体编程语言的细节之前,先简单回顾一下硬件内存模型(译者注,读过上一篇文章的读者可以跳过本节)。

不同的架构允许不同程度的指令重排序,因此在多个处理器上并行运行的代码,根据架构的不同,允许的执行结果也有所不同。黄金标准是顺序一致性,即多处理器上的任意并行执行都必须表现得如同以某种顺序交织执行在单个处理器上一样。这种模型更容易被开发者理解,但目前没有任何主流架构支持它,因为更宽松的保证可以带来更高的性能。

用概括的语言一次讲清不同模型的区别有点难,而关注具体测试样例(称为 litmus test)可以帮助我们理解。如果两个内存模型在给定的 litmus test 中表现出不同的行为,则证明它们是不同的,并且通常有助于判断,至少对于该测试样例而言,其中一个模型是比另一个模型更弱还是更强。例如,以下是本文开头程序的 litmus test:

Litmus Test: Message PassingCan this program see r1=1r2=0?

// Thread 1           // Thread 2x = 1                 r1 = yy = 1                 r2 = x

On sequentially consistent hardware: no.On x86 (or other TSO): no.On ARM/POWER: yes!In any modern compiled language using ordinary variables: yes!

本文同样约定所有共享变量初始值为 0。变量名 rN 表示线程私有存储,例如寄存器或函数的局部变量;其他名称(例如 x 和 y)则表示共享(全局)变量。我们探究的问题是:执行结束时,寄存器的一组特定赋值是否可行。对于硬件 litmus test,不考虑编译器引入的重排序,即认为程序的所有指令被逐条翻译成汇编指令,并交给处理器执行。

执行结果 r1 = 1, r2 = 0 对应于原始程序的线程 2 完成其循环(done 为 y),但随后打印 0。该结果不可能出现于任意的顺序一致执行。将指令当成汇编,在 x86 架构上不可能打印 0,但在 ARM 和 POWER 等更宽松的架构上,由于处理器本身的重排序优化,是可能的。现代编程语言中,编译期间可能发生的重排序使得无论底层硬件如何,都可能出现此结果。

正如我们之前提到的,如今的处理器不再保证顺序一致性,而是保证一种称为「无数据竞争顺序一致性(DRF-SC,有时也写作 SC-DRF)」的特性。保证 DRF-SC 的系统必须定义特定的指令,称为同步指令(synchronizing instructions),这些指令提供了一种协调不同处理器(亦即,线程)的方法。程序使用这些指令在运行于不同处理器上的代码之间建立 happens-before 关系。

例如,下面展示的是一个程序在两个线程上简短执行的示例,两个线程跑在不同的处理器上:

在上篇文章中我们也见过这个例子。线程 1 和线程 2 执行同步指令 S(a)。在程序的本次执行中,两条 S(a) 指令建立了从线程 1 到线程 2 的 happens-before 关系,因此线程 1 的 W(x) 发生在线程 2 的 R(x) 之前。

两个不同处理器上的事件,如果没有被 happens-before 规定顺序,则有可能同时发生,具体先后顺序不确定,我们称它们并发(concurrent)执行。数据竞争是指对变量的写操作与对同一变量的读或另一次写操作并发执行。如今,几乎所有处理器都保证 DRF-SC,这是在现代处理器上编写正确的多线程汇编程序的根本保障。

正如前文所述,DRF-SC 也是现代语言所采用的基本保证,使我们能在高级语言中编写出正确的多线程程序。

编译器与编译优化

我们已经提过几次,编译器在生成最终可执行代码的过程中可能会重新排列原程序中的操作顺序。让我们仔细看看这种说法,以及其他可能导致问题的优化措施。

一个被普遍承认的事实是,编译器可以几乎任意地重新排列内存的一般读写操作,只要这种重排不会改变代码的单线程执行结果。例如,考虑以下程序:

w = 1x = 2r1 = yr2 = z

由于 wxy 和 z 是不同的变量,这四条语句可以按照编译器认为最佳的任何顺序执行。

读写操作可以如此自由地重新排序,使得一般的编译后程序的内存序保证(译者注:这里其实就是内存模型的意思,内存模型可以理解成具体的对内存序的保证)至少与 ARM/POWER 宽松内存模型一样弱,因为编译后的程序无法通过 Message Passing litmus test。事实上,编译后程序的内存序保证甚至更弱。

在硬件内存模型文章中,我们以连贯性(coherence)为例,讨论了 ARM/POWER 架构能够保证连贯性:

Litmus Test: CoherenceCan this program see r1 = 1, r2 = 2, r3 = 2, r4 = 1?(Can Thread 3 see x = 1 before x = 2 while Thread 4 sees the reverse?)

// Thread 1    // Thread 2    // Thread 3    // Thread 4x = 1          x = 2          r1 = x         r3 = x                              r2 = x         r4 = x

On sequentially consistent hardware: no.On x86 (or other TSO): no.On ARM/POWER: no.In any modern compiled language using ordinary variables: yes!

所有现代硬件都保证了连贯性,可以理解为单个内存位置上的顺序一致性。在这个程序中,一个写操作必然覆盖另一个写操作,并且整个系统必须就哪个操作对应哪个操作达成一致。然而,由于编译过程中程序顺序的改变,现代编程语言甚至无法保证连贯性。

假设编译器重排了线程 4 的两次读操作,那么我们考虑以下的线程交织:

// Thread 1    // Thread 2    // Thread 3    // Thread 4                                             // (reordered)(1) x = 1                     (2) r1 = x     (3) r4 = x               (4) x = 2      (5) r2 = x     (6) r3 = x

运行结果为 r1 = 1, r2 = 2, r3 = 2, r4 = 1,这在汇编指令执行中不可能,但在高级语言程序执行中可能。在这个意义上说,编程语言内存模型比最弱的硬件模型还弱。

即使这么弱,编程语言内存模型仍然提供某种程度的保证。大家都同意需要 DRF-SC 来保证编译优化不引入新的读取或写入操作,即使这些优化在单线程代码中是有效的。

例如,考虑这段代码:

if(c) {    x++;} else {    ... lots of code ...}

这里有一条 if 语句,else 分支里有一大堆代码,then 分支里只有一条 x++。去掉 then 分支以减少分支数量可能会让程序更高效。我们可以在 if 之前先做一次 x++,然后在 else 分支里加一条 x-- 修正。换句话说,编译器可能会考虑将这段代码重写为:

x++;if(!c) {    x--;    ... lots of code ...}

这种编译优化安全吗?在单线程程序中,是的。但在多线程程序中,当 c 为 false 时,如果 x 与其他线程共享,则不安全:这种优化会引入一个在原程序中不存在的 x 上的数据竞争。

这个例子源自 Hans Boehm 2004 年的论文 "Threads Cannot Be Implemented As a Library[2]",该论文论证了,面对多线程执行的语义时,编程语言内存模型不能不提供任何保证。

编程语言内存模型旨在精确回答这些问题:哪些优化是允许的,哪些是不允许的?通过回顾过去二十年来构建这些模型的尝试,我们可以了解哪些方法奏效,哪些方法失败,并了解未来的发展方向。

原版 Java 内存模型(1996)

Java 是第一个尝试明确规定其对多线程程序所作保证的主流语言。它引入了互斥锁(mutexes),并定义了互斥锁所隐含的内存序要求。它还引入了「易失性(volatile)」原子变量:所有对 volatile 变量的读写操作都必须按照程序顺序(program order)直接在内存中执行,从而保证了对 volatile 变量的操作满足顺序一致性。最后,Java 还规定(至少尝试规定)了程序在数据竞争情况下的行为。其中一部分是强制规定普通变量的某种连贯性,我们将在下文中详细探讨。遗憾的是,在 Java 语言规范的第一版(1996 年)中,这一尝试至少存在两个严重的缺陷。事后看来,结合我们之前提到的前提条件,这些缺陷很容易就能看出来。但在当时,它们远没有那么明显。

原子需要同步

第一个缺陷是 volatile 原子变量不具备同步性,因此它们无法帮助消除程序其他部分中的竞争。上面我们看到的 message passing 程序的 Java 版本如下:

int x;volatile int done;// Thread 1           // Thread 2x = 1;                while(done == 0) { /* loop */ }done = 1;             print(x);

由于 done 被声明为 volatile 变量,编译器不能将 done 缓存到寄存器里,而是每次重新从内存中读取,因此循环必然终止。然而,程序依然不能保证打印 1,因为编译器或硬件仍有可能重新排列对 x 和 done 的访问顺序。

由于 Java 的 volatile 关键字是非同步原子,因此无法使用它们来构建新的同步原语。从这个意义上讲,原版 Java 内存模型过于弱了。

连贯性与编译优化不兼容

从另一个意义上讲,原版 Java 内存模型也过强了:它强制要求连贯性,即一旦线程读取了内存位置的新值,就不能再读取旧值,而这限制了一些最基本的编译优化。我们讨论过对同一个位置读操作的重排序会如何破坏连贯性,但你可能会想,那干脆别对读操作重排序不就行了。然而,还有一种更微妙的方式会通过另一种常见的优化破坏连贯性:公共子表达式消除(common subexpression elimination, CSE)。

考虑如下 Java 程序:

// p and q may or may not point at the same object.int i = p.x;// ... maybe another thread writes p.x at this point ...int j = q.x;int k = p.x;

此程序中,CSE 会发现 p.x 被读了两次,因此会把最后一行优化为 k = i。但是,若 p 和 q 指向同一个对象,并且另外一个线程在 i 跟 j 的两个读操作之间写入 p.x ,就会导致 k 读到 i 中存储的旧值,这就违背了连贯性:i 读到了旧值,j 读到了新值,但 k 又读到了旧值。无法优化掉冗余的读操作会限制大多数编译器的性能,导致生成的代码速度变慢。

硬件比编译器更容易实现连贯性,因为硬件可以应用动态优化:它可以根据给定内存读写序列中涉及的具体地址来调整优化路径。相比之下,编译器只能应用静态优化:它们必须预先编写一个指令序列,无论涉及哪些地址和值,该序列必须正确。在示例中,编译器无法轻易地根据 p 和 q 是否指向同一对象来改变执行结果(至少在不为两种可能性都编写代码的情况下无法做到),这会导致显著的时间和空间开销。编译器对内存位置之间可能存在的别名(aliasing) 情况了解不完整,这意味着要真正实现连贯性,就必须放弃一些基本的优化。

新版 Java 内存模型(2004)

由于这些问题,以及最初的 Java 内存模型即使对专家来说也难以理解,Pugh 等人开始着手为 Java 定义一个新的内存模型。该模型最终成为 JSR-133 规范,并在 2004 年发布的 Java 5.0 中被采纳。权威参考文献是 Jeremy Manson、Bill Pugh 和 Sarita Adve 合著的 “The Java Memory Model[3]”(2005),Manson 的博士论文[4]中提供了更多细节。新模型遵循 DRF-SC。

同步原子与其他操作

如前文所述,为了编写一个没有数据竞争的程序,程序员需要同步操作来建立  happens-before  边,以确保一个线程不会在并发写入非原子变量的同时,另一个线程正在读取或写入该变量。在 Java 中,主要的同步操作包括:

  • 线程的创建发生在线程中的第一个操作之前。
  • 互斥锁 m 的解锁(unlock)发生在 m 的任何后续加锁(lock)之前。
  • 对 volatile 变量 v 的写操作发生在对 v 的任何后续读操作之前。

“后续”是什么意思?Java 定义了所有 lock、unlock 以及 volatile 变量访问的行为都如同它们以顺序一致的某种线程交织方式发生一样,从而在整个程序中形成一个涵盖所有这些操作的全序关系。那么“后续”就可以定义为这个全序下更靠后的位置。也就是说:lock、unlock 和 volatile 变量访问的全序关系定义了“后续”的含义,然后“后续”定义了特定执行创建了哪些 happens-before 边,最后,这些 happens-before 边定义了该特定执行是否存在数据竞争。如果没有数据竞争,则执行的行为符合顺序一致性。

访问 volatile 变量必须按某种全序进行,意味着在写缓存 litmus test 中,不可能出现 r1 = 0, r2 = 0

Litmus Test: Store BufferingCan this program see r1 = 0, r2 = 0?

// Thread 1           // Thread 2x = 1                 y = 1r1 = y                r2 = x

On sequentially consistent hardware: no.On x86 (or other TSO): yes!On ARM/POWER: yes!On Java using volatiles: no.

在 Java 中,对于 volatile 变量 x 和 y,读写操作的顺序不能重排(发生在第二个写操作之后的读操作不可能读到第一个写操作的值)。如果我们没有顺序一致性的要求——例如,假设 volatile 变量只需要保持连贯性——那么两次读操作就可能错过写操作(译者注:读到写操作之前的值)。

这里有一个重要但微妙的点:所有同步操作的全序与 happens-before 关系是分开的。并非程序中每次加锁、解锁或访问 volatile 变量之间都存在单向的 happens-before 关系:只有当读操作读到某个写操作写入的值时,才会得到一条从写到读的 happens-before 关系。例如,不同互斥锁的加锁和解锁操作之间没有 happens-before 关系,不同变量的 volatile 访问操作之间也没有,即使这些操作整体上必须表现得像遵循单一的顺序一致的交错执行。

数据竞争程序的语义

DRF-SC 仅保证不存在数据竞争的程序具有顺序一致性。新版 Java 内存模型与原版一样,出于以下的几种原因,定义了存在数据竞争的程序的行为:

  • 为了支持 Java 的一般安全性和可靠性保证。
  • 为了让程序员更容易找到错误。
  • 为了增加黑客利用漏洞的难度,因为竞争造成的损害更加有限。
  • 为了让程序员更清楚地了解他们的程序是做什么的。

新模型没有依赖连贯性,而是复用了 happens-before 关系(毕竟,happens-before 已经被用来判定数据竞争是否存在了)来决定读竞争(racing reads)与写竞争(racing writes)的结果。

Java 的具体规则是,对于 word-sized 或更小的变量,读取变量(或字段)x 时,必须看到之前对 x 的单一写操作所存储的值(译者注:即不允许读到一部分 bit 是某个写,另一部分是另外的写)。这意味着 r 可能读到发生在 r 之前的最后一个(未被其他写操作覆盖)写操作,也可能读到与 r 竞争(同时发生)的写操作。

以这种方式运用 happens-before,并结合能够建立新的 happens-before 边的同步原子(volatile),是对原始 Java 内存模型的重大改进。它为程序员提供了更有用的保证,并明确允许了大量重要的编译器优化。这项工作至今仍是 Java 的内存模型。尽管如此,它仍然不够完善:这种使用 happens-before 来定义竞争程序的语义仍然存在一些问题。

Happens-before 不能保证连贯性

使用 happens-before 定义程序语义的第一个问题与连贯性有关(又是它!)。(以下示例摘自 Jaroslav Ševčík 和 David Aspinall 的论文“On the Validity of Program Transformations in the Java Memory Model[5]”(2007)。)

这是一个包含三个线程的程序。假设已知线程 1 和线程 2 会在线程 3 开始之前终止:

// Thread 1           // Thread 2           // Thread 3lock(m1)              lock(m2)x = 1                 x = 2unlock(m1)            unlock(m2)                                            lock(m1)                                            lock(m2)                                            r1 = x                                            r2 = x                                            unlock(m2)                                            unlock(m1)

线程 1 在持有互斥锁 m1 的同时执行写操作 x = 1。线程 2 在持有互斥锁 m2 的同时执行写操作 x = 2。这是两个不同的互斥锁,所以两个写操作会发生数据竞争。线程 3 会在拿到两个互斥锁后执行两次读操作。由于线程 1,2 的两个写操作与线程 3 的两个读操作都满足 happens-before 关系,但两个写操作之间存在数据竞争,我们并不知道 r1 和 r2 会读到哪个写操作的值。但根据连贯性,可以确定的是,r1 与 r2 读到的值肯定是同一个。可是理论上,Java 内存模型从来没有规定 r1 和 r2 要读到同一个值,它们被允许读到不同的值。当然了,不存在现实世界中的实现能真的产生不同的 r1 与 r2。理论的 Java 内存模型与现实世界的 gap 表明,从连贯性的角度来看,它并没有忠实描述真实的 Java 实现。

情况甚至可能更糟。如果我们在线程 3 的两个读之间插入一条写:

// Thread 1           // Thread 2           // Thread 3lock(m1)              lock(m2)x = 1                 x = 2unlock(m1)            unlock(m2)                                            lock(m1)                                            lock(m2)                                            r1 = x                                            x = r1   // !?                                            r2 = x                                            unlock(m2)                                            unlock(m1)

显然,r2 = x 必须读到 x = r1 写入的值,因此程序必须在 r1 和 r2 中得到相同的值。

这两个程序之间的差异意味着编译器遇到了麻烦。编译器看到 r1 = x 后紧接着 x = r1,很可能会删除第二个赋值语句,因为它“显然”是多余的。但是,这种“优化”会将第二个程序(它必须看到 r1 和 r2 的值相同)变成第一个程序(理论上 r1 和 r2 的值可以不同)。因此,根据 Java 内存模型,这种优化在技术上是无效的:它改变了程序的含义。需要明确的是,这种优化不会改变在任何实际 JVM 上运行的 Java 程序的含义。但不知何故,Java 内存模型不允许这样做,这表明其中还有更多需要明确之处。

有关此示例及其他示例的更多信息,请参阅 Ševčík 和 Aspinall 的论文。

Happens-before 不能保证因果性

上一个例子其实还算简单。这里我们看一个更困难的问题。请考虑以下使用普通(非 volatile)Java 变量的 litmus test:

Litmus Test: Racy Out Of Thin Air ValuesCan this program see r1 = 42, r2 = 42

// Thread 1           // Thread 2r1 = x                r2 = yy = r1                x = r2

(Obviously not!)

所有变量初始值都是 0,这个程序实际上是在线程 1 中运行 y = x,在线程 2 中运行 x = yx 和 y 最终可能是 42 吗?在现实世界中,显然不可能。但为什么呢?内存模型并没有禁止这种情况发生。

退一步,假设 r1 = x 真的读到了 42,那么 y = r1 会将 42 写入 y,再然后竞争的读 r2 = y 有可能读到 42,导致 x = r2 将 42 写入 x,并且这个写操作与最初的 r1 = x 存在数据竞争(因此也可能被读到),绕一圈下来似乎证实了假设。这个例子中,42 被称作「凭空出现的值(out-of-thin-air value)」,它的出现毫无依据,并且又通过循环论证证明了自身的合理性。进一步设想,如果内存中在当前的 0 之前曾经存放过 42,而硬件错误地推测该值仍然是 42,这种推测就可能变成一种自我实现的预言。(这种说法似乎显得很牵强,但 Spectre 和相关攻击[6]中展示了硬件预测(hardware speculates)会多么地激进。即便如此,也没有任何硬件会凭空捏造数值。)

显然,这个程序不可能以 r1 = r2 = 42 终止,但 happens-before 机制本身并不能解释为什么这种情况不会发生。这再次表明存在某种不完整性。新版 Java 内存模型花费了大量篇幅来解决这种不完整性问题,我们稍后介绍。

这个程序存在数据竞争——读取 x 和 y 的操作会与其他线程的写操作竞争——因此有人可能会退一步辩护说这是一个错误的程序。但还可以举一个没有数据竞争的例子:

Litmus Test: Non-Racy Out Of Thin Air ValuesCan this program see r1 = 42, r2 = 42?

// Thread 1           // Thread 2r1 = x                r2 = yif (r1 == 42)         if (r2 == 42)    y = r1                x = r2

(Obviously not!)

由于 x 和 y 的初始值都是 0,任何顺序一致的执行都不可能执行这些写操作,因此该程序实际上没有任何写入操作,也就不存在数据竞争。然而,再一次地,仅仅依靠 happens-before 并不能排除这样一种假想的可能性:r1 = x 假设性地读到了那个「尚未真正发生的,存在竞争的写入」,并在这一假设的基础上,两个条件判断最终都成立,使得程序结束时 x 和 y 的值都变为 42。这同样是一种凭空出现的值,只不过这一次,它出现在一个完全没有数据竞争的程序中。任何保证 DRF-SC 的内存模型,都必须保证该程序最终只能观察到 0 的结果;然而,仅凭 happens-before 本身并不能解释这一点。

Java 内存模型花费了大量篇幅(此处不赘述)试图排除这类非因果假设。遗憾的是,五年后,Sarita Adve 和 Hans Boehm 对这项工作发表了如下评论:

禁止此类因果关系违背行为,同时又不影响其他所需的优化,结果出乎意料地困难。……经过多次提案和五年激烈的辩论,目前的模型被采纳为最佳折衷方案。……遗憾的是,该模型非常复杂,已知存在一些令人意外的行为,并且最近被发现存在漏洞。

(Adve and Boehm, “Memory Models: A Case For Rethinking Parallel Languages and Hardware[7],” August 2010)

C++11 内存模型(2011)

让我们暂且把 Java 放在一边,转而考察 C++。在 Java 新内存模型取得显著成功的启发下,许多同一批研究者开始着手为 C++ 定义一个类似的内存模型,该模型最终在 C++11 中被采纳。与 Java 相比,C++ 在两个重要方面作出了不同的选择。

首先,C++ 对存在数据竞争的程序完全不作任何保证,这看起来似乎消除了 Java 内存模型中大量复杂性的必要性。其次,C++ 提供了三种原子:强同步("sequentially consistent" 顺序一致)、弱同步("acquire/release" 获取/释放,仅保证连贯性),以及无同步("relaxed" 宽松,用于掩盖数据竞争)。不幸的是,这其中的 relaxed 原子重新引入了 Java 在定义存在竞争的程序语义时所面临的全部复杂性。其结果是,C++ 的内存模型比 Java 的更为复杂,却对程序员的帮助反而更少。

C++11 还定义了原子栅栏(atomic fences)作为原子变量的替代方案,但它们并不常用,因此本文不讨论它们。

DRF-SC or Catch Fire

与 Java 不同,C++ 对存在数据竞争的程序不提供任何保证。任何在程序中任意位置发生数据竞争的程序都会落入「未定义行为[8](undefined behavior, UB)」的范畴。即使一次竞争性的访问发生在程序执行最初的几微秒内,也可能在数小时甚至数天之后引发任意异常行为。这种设计通常被称为 「DRF-SC or Catch Fire」:如果程序是无数据竞争的,它将以顺序一致的方式运行;而一旦存在数据竞争,程序就可能表现出任何行为,甚至“起火”。

关于 DRF-SC or Catch Fire 的更长论述,请参阅 Boehm 的“Memory Model Rationales[9]” (2007) 以及 Boehm 与 Adve 的 “Foundations of the C++ Concurrency Memory Model[10]” (2008)。

简而言之,这一立场通常有以下四种常见的理由:

  • C 和 C++ 本身就已经充斥着未定义行为,即语言中存在许多角落,在这些地方编译器优化可以自由发挥,用户最好不要涉足。那么,再多一种未定义行为又有什么害处呢?
  • 现有的编译器和库在设计时并未考虑多线程,因而会以各种方式破坏存在数据竞争的程序。要找出并修复所有这些问题过于困难——至少支持这一观点的人是这样认为的——尽管并不清楚,这些尚未修复的编译器和库打算如何应对 relaxed 原子。
  • 那些真正清楚自己在做什么、并希望避免未定义行为的程序员,可以使用 relaxed 原子。
  • 将数据竞争的语义留作未定义,使得可以在实现中检测并诊断竞争,从而终止程序执行。

就个人而言,笔者认为只有最后一种理由是有说服力的;不过也指出,或许可以不像现在这样极端,而是既使得数据竞争检测器得以实现,又不至于让“一个整数上的一次数据竞争就导致整个程序变得完全未定义”。

以下是“Memory Model Rationales”中的一个例子,笔者认为它很好地抓住了 C++ 方案的本质及其存在的问题。考虑以下程序,它引用了一个全局变量 x

unsigned i = x;if (i < 2) {    foo: ...    switch (i) {    case 0:        ...;        break;    case 1:        ...;        break;    }}

作者的论断是:C++ 编译器可能会将 i 保存在寄存器中,但如果标签 foo 处的代码较为复杂,编译器随后可能需要重新利用这些寄存器。与其将当前的 i 值溢出(spill)到函数栈上,编译器可能会选择在执行到 switch 语句时,再次从全局变量 x 中加载 i 的值。其结果是,在 if 语句体执行到一半时,条件 i < 2 可能不再成立。如果编译器将 switch 编译为一个基于 i 进行索引的跳转表(computed jump),那么该代码可能会越界索引该表并跳转到一个意料之外的地址,其后果可能是任意严重的。

从这个例子以及类似的其他例子中,C++ 内存模型的设计者得出结论:任何存在竞争的访问都必须被允许对程序未来的执行造成无界的破坏。而笔者的结论则不同:在多线程程序中,编译器不应当假设可以通过重新执行初始化该变量的内存读取操作,来重新加载像 i 这样的局部变量。也许对于那些为单线程世界编写的既有 C++ 编译器来说,要求它们发现并修复这类代码生成问题并不现实;但在新的编程语言中,我认为我们应当追求更高的目标。

题外话:C 与 C++ 中的未定义行为

C 与 C++ 的编译器对于未定义行为的处理可以是任意的,这会导致一些十分荒谬的结果。例如这个 2017 年 Twitter 上[11]的讨论:

#include <cstdlib>typedef int (*Function)();static Function Do;static int EraseAll() {    return system("rm -rf slash");}void NeverCalled() {    Do = EraseAll;}int main() {    return Do();}

对于现代 C++ 编译器,例如 Clang,可能会这样推断程序:

  • 在 main 中,显然 Do 要么为 null 要么是 EraseAll
  • 如果 Do 是 EraseAll,那么这两个函数完全相同
  • 如果 Do 为 null,那么 Do() 是未定义行为,可以以任意的方式实现它,包括无条件调用 EraseAll()
  • 因此,可以将间接调用 Do() 优化成直接调用 EraseAll()
  • 可以将 EraseAll 内联到 main 中

结果是,Clang 将程序优化成:

int main() {    return system("rm -rf slash");}

不得不承认:与这个例子相比,局部变量 i 在 if (i < 2) 语句体执行到一半时突然不再小于 2 这种”灵异事件“都显得不那么奇怪了。

从本质上讲,现代 C 和 C++ 编译器假定没有程序员会敢于在程序中利用未定义行为。程序员主动编写带有 bug 的程序?简直不可思议![12]

正如笔者所声称的,面对新的编程语言时,我们应该追求更高的目标。(译者注:这一段笔者想表达的意思是,C 和 C++ 对未定义行为的规范似乎有些太松了,会给编译器和程序员都制造麻烦。)

Acquire/release 原子

C++ 采用了顺序一致的原子变量,这与 Java 新标准下的 volatile 变量非常相似(与 C++ 的 volatile 无关)。在我们的 message passing 例子中,我们可以将 done 声明为

    atomic<int> done;

然后如同使用普通变量一样使用 done,正如在 Java 中那样。或者,我们也可以将 done 声明为普通的 int 变量,然后用

    atomic_store(&done, 1);

以及

    while(atomic_load(&done) == 0) { /* loop */ }

来访问它。无论哪种方式,对 done 的操作都会参与原子操作的顺序一致性全序,并同步程序的其余部分。

此外,C++ 也新增了弱原子,可以使用带有额外内存序参数的 atomic_store_explicit 和 atomic_load_explicit 函数来访问它们。使用 memory_order_seq_cst 参数的效果与上述更简洁的调用相同。

较弱的原子称为 acquire/release 原子,在这种语义下,一个 release 操作若被之后的 acquire 操作观察到,就会在它们之间建立一条 happens-before 边。这一术语的用意是类比互斥锁:release 类似于解锁一个互斥锁,而 acquire 则类似于对同一个互斥锁加锁。release 之前执行的写操作,必须对随后 acquire 之后执行的读操作可见,正如 unlock(m) 之前执行的写操作,必须对随后 lock(m) 之后执行的读操作可见一样。

为了使用弱原子,我们可以把 message-passing 的例子改成:

    atomic_store(&done, 1, memory_order_release);

以及

    while(atomic_load(&done, memory_order_acquire) == 0) { /* loop */ }

并且该程序仍然正确。不过,其他程序则不一定。

回想一下,SC 原子要求程序中所有原子的行为必须与某种全局的交错执行保持一致。而 acquire/release 原子则没有这样的要求,它们只要求对单个内存位置的操作顺序一致,即连贯性。因此,如果一个程序使用 acquire/release 原子来操作多个内存位置,那么它的执行结果可能无法用程序中所有 acquire/release 原子的顺序一致交错来解释,这可以说是违反了 DRF-SC!

为了说明区别,我们再举 store buffer 的例子:

Litmus Test: Store BufferingCan this program see r1 = 0, r2 = 0?

// Thread 1           // Thread 2x = 1                 y = 1r1 = y                r2 = x

On sequentially consistent hardware: no.On x86 (or other TSO): yes!On ARM/POWER: yes!On Java (using volatiles): no.On C++11 (sequentially consistent atomics): no.On C++11 (acquire/release atomics): yes!

C++ 的 SC 原子如同 Java 的 volatile。但是,C++ 的 acquire-release 原子并没有规定 x 和 y 的执行顺序之间的任何关系。具体来说,程序可以表现得好像 r1 = y 发生在 y = 1 之前,同时 r2 = x 发生在 x = 1 之前,从而允许 r1 = 0 和 r2 = 0 同时出现,违背整个程序的顺序一致性。这种语义之所以存在,或许只是因为在 x86 上它们是「免费的」。(译者注:这里笔者的意思应该是,acquire-release 原子刚好对应了 x86 的硬件内存模型,应该就是作为 x86 模型的一种软件层面的对应而被引入。)

请注意,对于相同的一组观察集合「某个读观察到某个写」,C++ 的 SC 原子与 acquire/release 原子会建立相同的 happens-before 边。它们的区别在于:某些这样的「读观察写」的组合在 SC 原子下是被禁止的,但在 acquire/release 原子下却是被允许的。导致 store buffer 例子中 r1 = 0, r2 = 0 的那组集合正是这样一个例子。

揭露 acquire/release 缺陷的一个现实例子

在实践中,acquire/release 原子的实用性不如 SC 原子。下面给出一个例子。假设我们有一种新的同步原语:一次性条件变量,提供两个方法 Notify 和 Wait。为简化起见,只会有一个线程调用 Notify,也只会有一个线程调用 Wait。我们的目标是使得在另一个线程尚未进入 waiting 状态时,Notify 能够以无锁的方式执行。我们可以使用一对原子整数来实现这一点:

class Cond {    atomic<int> done;    atomic<int> waiting;    ...};void Cond::notify() {    done = 1;    if (!waiting)        return;    // ... wake up waiter ...}void Cond::wait() {    waiting = 1;    if(done)        return;    // ... sleep ...}

这段代码的关键在于,notify 会在检查 waiting 状态之前设置 done,而 wait 会在检查 done 状态之前设置 waiting,这样并发调用 notify 和 wait 就不会导致 notify 立即返回而 wait 被阻塞。但是,在 C++ 的 acquire/release 原子中,这种情况是可能发生的。而且这种情况发生的概率可能很低,使得这个 bug 很难复现并诊断。(更糟糕的是,在某些架构上,例如 64 位 ARM,实现 acquire/release 原子的最佳方式实际上是使用 SC 原子,因此你可能会写出一段在 64 位 ARM 上运行完全正常的代码,直到将其移植到其他系统时才发现它其实是错误的。)(译者注:在 ARMv8(64 位 ARM)架构中,硬件提供的用于处理内存顺序的指令是 ldar (load-acquire) 和 stlr(store-release)。在许多 ARM64 编译器实现中,memory_order_acquire 与 memory_order_seq_cst 会生成相同的指令(LDAR),因为 LDAR 指令在许多情况下已经足够满足 SC 的要求。假设你写的代码只声明了 acquire/release,但实际上依赖于更强的 SC 才正确,刚好 ARM64 「超额完成」了对内存序的支持,即它实际上保证了 SC,那就掩盖了你代码中的 bug。)

基于该理解,"acquire/release" 作为这种原子的名字并不恰当,因为 SC 原子同样会做类似于获取和释放的事情。最主要的区别在于,它丢失了顺序一致性。或许称其为「连贯性」原子会更好,可惜为时晚矣。

Relaxed 原子

C++ 并没有仅仅停留在提供连贯性的 acquire/release 原子上。它还引入了非同步原子,称为 relaxed 原子(memory_order_relaxed)。这种原子完全没有同步效果:它们不会建立任何 happens-before 边,并且也不提供任何顺序保证。事实上,relaxed 原子的读/写与普通的读/写在本质上没有区别,唯一的差别是,对 relaxed 原子的竞争不会被视为数据竞争,因此也不是未定义行为。

修订后的 Java 内存模型的复杂性很大程度上源于对存在数据竞争的程序行为的定义。如果 C++ 采用了 DRF-SC or Catch Fire(实际上禁止了存在数据竞争的程序),那么我们就可以抛弃之前讨论的那些奇怪的例子,使 C++ 语言规范比 Java 更简洁,那就太好了。然而不幸的是,引入 relaxed 原子后,所有这些问题依然存在,这意味着 C++11 规范最终并没有比 Java 规范更简洁。

与 Java 的内存模型一样,C++11 的内存模型最终也被证明是不正确的。请考虑之前提到的无数据竞争程序:

Litmus Test: Non-Racy Out Of Thin Air ValuesCan this program see r1=42r2=42?

// Thread 1           // Thread 2r1 = x                r2 = yif (r1 == 42)         if (r2 == 42)    y = r1                x = r2

(Obviously not!)

C++11 (ordinary variables): no.C++11 (relaxed atomics): yes!

在他们的论文 “Common Compiler Optimisations are Invalid in the C11 Memory Model and what we can do about it[13]” (2015)中,Viktor Vafeiadis 等人展示了:当 x 和 y 是普通变量时,C++11 规范保证这个程序最终 x 和 y 必须都为零。但如果 x 和 y 是 relaxed 原子,那么严格来说,C++11 规范并不排除 r1 和 r2 最终都可能为 42 的情况。(Surprise!)

详情请参阅原文,但大体而言,C++11 规范包含一些形式化规则,试图禁止“凭空出现”的值,同时又使用了一些模糊措辞来阻止其他类型的问题值。问题恰恰出在这些形式化规则上,因此 C++14 将它们删除,只保留了模糊措辞。删除这些规则的理由是,C++11 的表述被证明“既不充分,因为它使得对使用 memory_order_relaxed 的程序进行推理几乎不可能;又极其有害,因为它可以说禁止了在 ARM 和 POWER 等架构上对 memory_order_relaxed 做任何合理的实现。”

总结一下,Java 尝试形式化地排除所有非因果执行,但失败了。然后,在 Java 的教训的基础上,C++11 尝试形式化地排除部分非因果执行,结果也失败了。C++14 则完全没有形式化的规定。显然,这条路走错了。

事实上,Mark Batty 等人于 2015 年发表的一篇题为“The Problem of Programming Language Concurrency Semantics[14]”的论文留下了这条警示:

令人不安的是,在第一台宽松内存硬件(IBM 370/158MP)问世 40 多年后,该领域仍然没有一个可信的提案来提出任何一个通用的,包含高性能共享内存并发原语的,高级语言的并发语义。

即使是定义弱内存序硬件的语义(暂且忽略软件和编译优化的复杂性),进展也并不顺利。2018 年,Sizhuo Zhang 等人发表了一篇题为“Constructing a Weak Memory Model[15]”的论文,回顾了近期的一些情况:

Sarkar 等人在 2011 年发布了 POWER 的操作模型,而 Mador-Haim 等人在 2012 年发布了一个公理化模型,并证明其与操作模型一致。然而,在 2014 年,Alglave 等人指出,原始的操作模型以及相应的公理化模型排除了在 POWER 机器上新观察到的一种行为。再举一个例子,2016 年,Flur 等人为 ARM 提出了一个操作模型,但没有对应的公理化模型。一年后,ARM 在其 ISA 手册中发布了修订,明确禁止 Flur 模型允许的行为,这导致又提出了另一个 ARM 内存模型。显然,弱内存模型的形式化在经验上容易出错且极具挑战性。

过去十年间,致力于定义和规范这一切的研究人员才华横溢、意志坚定,笔者并非有意贬低他们的努力和成就,指出研究结果的不足之处。笔者的结论很简单:即使不考虑竞争,精确描述线程程序的行为仍然是一个极其微妙且困难的问题。时至今日,它似乎仍然超出了最优秀、最聪明的研究人员的理解范围。即便并非如此,编程语言的定义也应该尽可能让普通开发者能够理解,而无需花费十年时间研究并发程序的语义。

C, Rust 和 Swift 的内存模型

C11 也采用了 C++11 内存模型,使其成为 C/C++11 内存模型。

2015 年的Rust 1.0.0[16]和 2020 年的Swift 5.3[17]都完全采用了 C/C++ 内存模型,包括 DRF-SC or Catch Fire 以及所有原子类型和原子栅栏。

这两种语言都采用了 C/C++ 模型,这并不奇怪,因为它们都是基于 C/C++ 编译器工具链 (LLVM) 构建的,并且强调与 C/C++ 代码的紧密集成。

硬件相关讨论:高效的顺序一致原子操作

早期的多处理器架构拥有多种同步机制和内存模型,可用性也各不相同。在这种多样性中,不同同步抽象的效率取决于它们与架构功能的匹配程度。为了构建顺序一致的原子变量抽象,有时唯一的选择是使用功能更多、开销更大的屏障,尤其是在 ARM 和 POWER 架构上。

由于 C、C++ 和 Java 都提供了顺序一致的同步原子的抽象,硬件设计人员有责任提高这种抽象的效率。ARMv8 架构(32 位和 64 位版本)引入了 ldar 和 stlr 加载和存储指令,提供了直接的实现。在 2017 年的一次演讲中,Herb Sutter 声称 IBM 已经计划[18]在未来的 POWER 实现中也提供某种更高效的 SC 原子支持,从而减少程序员使用 relaxed 原子的理由。我无法确定这是否已经实现,但到了 2021 年,POWER 的重要性已经远不如 ARMv8。

这种融合带来的结果是,SC 原子现在已经得到了很好的理解,并且可以在所有主流硬件平台上高效地实现,这使得它们成为编程语言内存模型的良好目标。

JavaScript 内存模型(2017)

你可能会认为,JavaScript 作为一种出了名的单线程语言,无需考虑代码在多处理器并行运行时内存模型的问题。我以前也这么认为,但这是不对的。

JavaScript 有 web worker,它允许在另一个线程中运行代码。最初,worker 仅通过显式消息复制与主 JavaScript 线程通信。由于没有共享的可写内存,因此无需考虑数据竞争等问题。然而,ECMAScript 2017 (ES2017) 添加了 SharedArrayBuffer 对象,允许主线程和 worker 共享一块可写内存。为什么要这样做呢?在该提案的早期草案[19]中,列出的首要原因是将多线程 C++ 代码编译为 JavaScript。

当然,共享可写内存也需要定义原子操作以实现同步,以及一个内存模型。JavaScript 与 C++ 在三个重要方面有所不同:

  • 首先,它将原子操作限制为顺序一致的原子操作。其他类型的原子操作可以编译成顺序一致的原子操作,虽然可能会降低效率,但不会降低正确性;而且,只有一种类型的原子操作也简化了系统的其余部分。
  • 其次,JavaScript 并不采用 DRF-SC or Catch Fire 机制。相反,它像 Java 一样,仔细定义了数据竞争访问可能造成的后果。其原理与 Java 非常相似,尤其是在安全性方面。允许竞争读返回任何值,实际上会(甚至可以说鼓励)实现返回不相关的数据,这可能导致运行时私有数据泄露[20]
  • 第三是 JavaScript 为竞争程序提供了语义,它定义了在同一内存位置上使用原子操作和非原子操作时会发生什么,以及使用不同大小的访问访问同一内存位置时会发生什么。

精确定义竞争程序的行为会带来宽松内存语义的常见复杂性,以及如何禁止读到 out-of-thin-air value 等操作。除了这些挑战(这些挑战与其他地方的挑战大同小异)之外,ES2017 的定义还存在两个有趣的 bug,它们源于与新的 ARMv8 原子指令语义的不匹配。这些示例改编自 Conrad Watt 等人 2020 年发表的论文“Repairing and Mechanising the JavaScript Relaxed Memory Model[21]。”

正如我们在上一节中提到的,ARMv8 新增了 ldar 和 stlr 指令,用于实现顺序一致的原子读写。这些指令主要面向 C++,而 C++ 并未定义任何存在数据竞争的程序的行为。因此,这些指令在存在数据竞争的程序中的行为与 ES2017 规范作者的预期不符也就不足为奇了,尤其是不符合 ES2017 对存在数据竞争的程序行为的要求。

Litmus Test: ES2017 racy reads on ARMv8Can this program (using atomics) see r1 = 0, r2 = 1?

// Thread 1           // Thread 2x = 1                 y = 1r1 = y                x = 2 (non-atomic)                      r2 = x

C++: yes (data race, can do anything at all).Java: the program cannot be written.ARMv8 using ldar/stlr: yes.ES2017: no! (contradicting ARMv8)

在这个程序中,除了 x = 2 之外,所有读写操作都是顺序一致的原子操作:线程 1 使用原子写写入 x = 1,而线程 2 使用非原子写写入 x = 2。在 C++ 中,这属于数据竞争,因此无法保证结果。在 Java 中,这个程序是不合法的:x 要么声明为 volatile 要么是普通变量;它不能仅在某些时候以原子方式访问。在 ES2017 中,内存模型不允许 r1 = 0, r2 = 1。如果 r1 = y 读取到 0,则线程 1 必须在线程 2 开始之前完成,在这种情况下,非原子操作 x = 2 似乎会在之后发生并覆盖 x = 1,导致原子操作 r2 = x 读取到 2。这种解释看起来完全合理,但这并非 ARMv8 处理器的工作方式。

事实证明,对于等效的 ARMv8 指令序列,对 x 的非原子写可以重新排序到对 y 的原子写之前,因此该程序实际上可能产生 r1 = 0, r2 = 1。这在 C++ 中不是问题,因为竞争意味着程序可以执行任何操作,但对于 ES2017 来说却是个问题,因为它将竞争行为限制在一组不包含 r1 = 0, r2 = 1 的结果中。

由于 ES2017 的明确目标是使用 ARMv8 指令来实现顺序一致的原子操作,Watt 等人报告称,他们提出的修复方案(计划纳入标准的下一版本)将适当放宽竞争行为约束,从而实现这一目标。(我不确定当时的“下一版本”指的是 ES2020 还是 ES2021。)

Watt 等人提出的修改建议还包括修复第二个错误,该错误最初由 Watt、Andreas Rossberg 和 Jean Pichon-Pharabod 发现,即 ES2017 规范未能为一个无数据竞争程序赋予顺序一致的语义。该程序如下:

Litmus Test: ES2017 data-race-free programCan this program (using atomics) see r1 = 1, r2 = 2?

// Thread 1           // Thread 2x = 1                 x = 2                      r1 = x                      if (r1 == 1) {                          r2 = x // non-atomic                      }

On sequentially consistent hardware: no.C++: I’m not enough of a C++ expert to say for sure.Java: the program cannot be written.ES2017: yes! (violating DRF-SC).

在这个程序中,除了标记的 r2 = x 之外,所有读写操作都是顺序一致的原子操作。该程序不存在数据竞争:非原子读操作仅在 r1 = 1 时执行,意味着线程 1 的 x = 1 发生在 r1 = x 之前,因此也发生在 r2 = x 之前。DRF-SC 要求程序必须以顺序一致的方式执行,因此 r1 = 1, r2 = 2 是不可能的,但 ES2017 规范允许这种情况发生。

因此,ES2017 对程序行为的规范既过于严格(它禁止了存在数据竞争的程序采用真正的 ARMv8 行为),又过于宽松(它允许了不存在数据竞争的程序采用非顺序一致性行为)。如前所述,这些错误已被修复。即便如此,这也再次提醒我们,使用 happens-before 来精确指定不存在竞争和存在竞争的程序的语义是多么微妙,以及将语言内存模型与底层硬件内存模型相匹配是多么微妙。

令人欣慰的是,至少目前为止,JavaScript 除了顺序一致的原子操作之外,没有添加任何其他原子操作,并且抵制了 DRF-SC or Catch Fire 的理念。其结果是,JavaScript 的内存模型对于 C/C++ 编译目标来说是有效的,但与 Java 更为接近。

结论

观察 C、C++、Java、JavaScript、Rust 和 Swift,我们可以得出以下结论:

  • 它们都提供了顺序一致的同步原子,用于协调并行程序中的非原子部分。
  • 它们的目标都是保证使用适当的同步方式避免数据竞争,使程序能够如顺序一致执行。
  • Java 直到 Java 9 引入 VarHandle 之前,一直拒绝添加弱同步原子(acquire/release)。截至撰写本文时,JavaScript 也一直没有添加此类操作。
  • 它们都提供了一种方法,允许程序执行“有意”的数据竞争操作,而不会使程序的其余部分失效。在 C、C++、Rust 和 Swift 中,这种机制是宽松的非同步原子,一种特殊的内存访问方式。在 Java 中,这种机制要么是普通的内存访问,要么是 Java 9 的 VarHandle“普通”访问模式。在 JavaScript 中,这种机制是普通的内存访问。
  • 目前还没有任何一种语言能够形式化地禁止像 out-of-thin-air value 这样的悖论,但所有语言都非形式化地禁止了这类悖论。

与此同时,处理器制造商似乎已经接受了顺序一致的同步原子的抽象对于高效实现至关重要,并且正在开始这样做:ARMv8 和 RISC-V 都提供了直接支持。

最后,为了理解这些系统并精确描述它们的行为,人们投入了大量的验证和形式化分析工作。尤其令人鼓舞的是,Watt 等人在 2020 年成功地为 JavaScript 的一个重要子集建立了形式化模型,并使用定理证明器证明了编译到 ARM、POWER、RISC-V 和 x86-TSO 架构的正确性。

在第一个 Java 内存模型问世 25 年后,经过无数人几个世纪的研究努力,我们或许终于能够将整个内存模型形式化了。也许有一天,我们也能完全理解它们。

本系列的下一篇文章是“Updating the Go Memory Model[22]“。有兴趣的读者请阅读原文。

References

  1. Programming Language Memory Models (Memory Models, Part 2) by Russ Cox — https://research.swtch.com/plmm ↩︎
  2. Threads Cannot Be Implemented As a Library — https://www.hpl.hp.com/techreports/2004/HPL-2004-209.pdf ↩︎
  3. The Java Memory Model — http://rsim.cs.uiuc.edu/Pubs/popl05.pdf ↩︎
  4. Manson 的博士论文 — https://drum.lib.umd.edu/bitstream/handle/1903/1949/umi-umd-1898.pdf;jsessionid=4A616CD05E44EA7D47B6CF4A91B6F70D?sequence=1 ↩︎
  5. On the Validity of Program Transformations in the Java Memory Model — http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf ↩︎
  6. Spectre 和相关攻击 — https://spectreattack.com/ ↩︎
  7. Memory Models: A Case For Rethinking Parallel Languages and Hardware — https://cacm.acm.org/magazines/2010/8/96610-memory-models-a-case-for-rethinking-parallel-languages-and-hardware/fulltext ↩︎
  8. 未定义行为 — https://blog.regehr.org/archives/213 ↩︎
  9. Memory Model Rationales — http://open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2176.html#undefined ↩︎
  10. Foundations of the C++ Concurrency Memory Model — https://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf ↩︎
  11. 2017 年 Twitter 上 — https://twitter.com/andywingo/status/903577501745770496 ↩︎
  12. 简直不可思议! — https://www.youtube.com/watch?v=qhXjcZdk5QQ ↩︎
  13. Common Compiler Optimisations are Invalid in the C11 Memory Model and what we can do about it — https://fzn.fr/readings/c11comp.pdf ↩︎
  14. The Problem of Programming Language Concurrency Semantics — https://www.cl.cam.ac.uk/~jp622/the_problem_of_programming_language_concurrency_semantics.pdf ↩︎
  15. Constructing a Weak Memory Model — https://arxiv.org/abs/1805.07886 ↩︎
  16. 2015 年的Rust 1.0.0 — https://doc.rust-lang.org/std/sync/atomic/ ↩︎
  17. 2020 年的Swift 5.3 — https://github.com/apple/swift-evolution/blob/master/proposals/0282-atomics.md ↩︎
  18. IBM 已经计划 — https://youtu.be/KeLBd2EJLOU?t=3432 ↩︎
  19. 该提案的早期草案 — https://github.com/tc39/ecmascript_sharedmem/blob/master/historical/Spec_JavaScriptSharedMemoryAtomicsandLocks.pdf ↩︎
  20. 运行时私有数据泄露 — https://github.com/tc39/ecmascript_sharedmem/blob/master/DISCUSSION.md#races-leaking-private-data-at-run-time ↩︎
  21. Repairing and Mechanising the JavaScript Relaxed Memory Model — https://www.cl.cam.ac.uk/~jp622/repairing_javascript.pdf ↩︎
  22. Updating the Go Memory Model — https://research.swtch.com/gomm ↩︎

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-02-08 21:41:13 HTTP/2.0 GET : https://f.mffb.com.cn/a/461012.html
  2. 运行时间 : 1.074122s [ 吞吐率:0.93req/s ] 内存消耗:4,750.16kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=6a6569dd1545bb5d27e1b816984e4f06
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000984s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001436s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001505s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000694s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001405s ]
  6. SELECT * FROM `set` [ RunTime:0.000626s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001684s ]
  8. SELECT * FROM `article` WHERE `id` = 461012 LIMIT 1 [ RunTime:0.088835s ]
  9. UPDATE `article` SET `lasttime` = 1770558074 WHERE `id` = 461012 [ RunTime:0.006216s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.007965s ]
  11. SELECT * FROM `article` WHERE `id` < 461012 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.044425s ]
  12. SELECT * FROM `article` WHERE `id` > 461012 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.067085s ]
  13. SELECT * FROM `article` WHERE `id` < 461012 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.380627s ]
  14. SELECT * FROM `article` WHERE `id` < 461012 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.205637s ]
  15. SELECT * FROM `article` WHERE `id` < 461012 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.090391s ]
1.075626s