├── .gitattributes ├── .markdownlint.json ├── 10_Ideas_and_Inspiration.md ├── 1_Basic_of_Rust_Concurrency.md ├── 2_Atomics.md ├── 3_Memory_Ordering.md ├── 4_Building_Our_Own_Spin_Lock.md ├── 5_Building_Our_Own_Channels.md ├── 6_Building_Our_Own_Arc.md ├── 7_Understanding_the_Processor.md ├── 8_Operating_System_Primitives.md ├── 9_Building_Our_Own_Locks.md ├── LICENSE ├── README.md ├── assets └── css │ └── style.scss ├── attachment.md └── picture ├── raal_0301.png ├── raal_0302.png ├── raal_0303.png ├── raal_0304.png ├── raal_0305.png ├── raal_0401.png ├── raal_0701.svg ├── raal_0901.png ├── raal_0902.png ├── raal_10in01.png ├── raal_10in02.png ├── raal_10in03.png ├── raal_10in04.png ├── raal_10in05.png └── raal_10in06.png /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *.svg filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD013": false, 4 | "MD033": false 5 | } 6 | -------------------------------------------------------------------------------- /10_Ideas_and_Inspiration.md: -------------------------------------------------------------------------------- 1 | # 第十章:理念和灵感 2 | 3 | (英文版本) 4 | 5 | 有无数与并发相关的话题、算法、数据结构、轶事以及其它可能的章节都可能成为本书的一部分。然而,我们已经到了最后一章,我们即将结束我们的旅程,希望给你全新的可能性并对这些可能性感到兴奋,并准备在实践中应用新的知识和技能。 6 | 7 | 最终章节的目的是为了向你展示一些可以学习、探索和构建的想法,为你自己的创造和未来工作提供灵感。 8 | 9 | ## 信号量[^1] 10 | 11 | (英文版本) 12 | 13 | *信号量*实际上仅是有两个操作的计数器:*信号*(signal,也叫做 up 或 V)和*等待*(wait,也叫做 down 或 P)。signal 操作增加计数器到一个确定的最大值,而等待操作递减计数器的值。如果计数器是 0,wait 操作将阻塞并等待匹配的 signal 操作,以防止计数器将变成负数。这是一个灵活的工具,可以用于实现其它同步原语。 14 | 15 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in01.png) 16 | 17 | 信号量可以实现为用于计数器的 `Mutex` 以及用于等待操作的 `Condvar` 的组合。然而,有几种方式能更有效地实现它。更值得关注的是,在支持类 futex 操作([第八章“futex”](./8_Operating_System_Primitives.md#futex))的平台上,可以使用单个 AtomicU32(或者甚至 AtomicU8)更高效地实现。 18 | 19 | 最大值为 1 的信号量又是被称为*二进制*信号量,它可以用作构建其他原语的基石。例如,它可以通过初始化计数器初始化为 1、使用锁定的 wait 操作以锁定以及使用 signal 操作以解锁,来用作 mutex。通过将它初始化到 0,它也可以被用作信号,类似于条件变量。例如,在标准库 `std::thread` 的 `park()` 和 `unpark()` 函数可以实现与线程关联的二进制信号量上的 wait 和 signal 操作。 20 | 21 | > 注意,mutex 可以使用信号量来实现,而信号量可以使用 mutex(或者)来实现。建议避免使用基于 mutex 的信号量来实现基于信号量的 mutex,反之亦然。 22 | 23 | 进一步阅读: 24 | 25 | * [维基百科的信号量文章](https://en.wikipedia.org/wiki/Semaphore_(programming)) 26 | * [斯坦福大学关于信号量的课程的笔记](https://see.stanford.edu/materials/icsppcs107/23-Concurrency-Examples.pdf) 27 | 28 | ## RCU 29 | 30 | (英文版本) 31 | 32 | 如果你想要多个线程去(更多地)读和(少量地)更改一些数据,你可以使用 RwLock。当这些数据仅是单个整数时,你可以使用单个原子变量(例如 `AtomicU32`)去避免锁定,这样更有效。然而,对于巨大数据的分块,像有着很多字段的结构体,没有可用的原子类型允许对整个对象进行无锁原子操作。 33 | 34 | 就像计算机科学中的其他问题一样,该问题也可以通过**增加**间接的层的方式来解决。你可以使用原子变量去存储一个指向它的指针,而不是结构体本身。这仍然不允许你以原子地方式修改整个结构体,但它允许你以原子地方式替换整个结构体,这差不多。 35 | 36 | 这种模式通常称为 `RCU`,代表“读取、复制、更新”,这是替换数据所需要的步骤。读取指针后,可以将结构体复制进新的内存分配中,无需担心其他线程即可进行修改。准备就绪后,可以使用「比较并交换」操作([第二章节的“比较并交换”操作](./2_Atomics.md#比较并交换操作))来更新原子指针,如果没有其他线程在此期间替换数据,这将成功。 37 | 38 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in02.png) 39 | 40 | 关于 RCU 模式最有趣的部分是最后一步,它没有首字母缩略的单词:重新分配旧数据(deallocating the old data)。成功更新后,如果其他线程在更新前读取指针,它们仍然可能读取旧副本。你必须等待所有这些线程的完成,才能重新分配旧副本。 41 | 42 | 对于这个问题有很多可能的解决方案,包括引用计数(例如 `Arc`)、泄漏内存(忽视问题)、垃圾收集、冒险指针[^2](线程告诉其他线程它们当前正在使用什么指针的方式)以及静态状态跟踪(等待每个线程达到不再使用任何指针的点)。最后一个在某些情况下非常高效。 43 | 44 | 在 Linux 内核中的很多数据结构是基于 RCU 的,并且有很多关于它们实现细节有意思的讨论和文章,这可以提供一个很棒的灵感。 45 | 46 | 进一步阅读: 47 | 48 | * [维基百科的 RCU 文章](https://en.wikipedia.org/wiki/Read-copy-update) 49 | * [LWN 文章“从根本上说,什么是 RCU?”](https://lwn.net/Articles/262464/) 50 | 51 | ## 无锁链表 52 | 53 | (英文版本) 54 | 55 | 在基本的 RCU 模式上进行扩展,可以**增加**一个原子指针到结构体以指向下一个结构体,从而将其转换为*链表*。这允许线程以原子地方式**增加**或移除链表中的元素,而无需每次更新时复制整张表。 56 | 57 | 为了在表开始插入一个新元素,你仅需要分配该元素并将它的指针指向列表中的第一个元素,然后原子更新初始化指针以指向你最新分配到元素。 58 | 59 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in03.png) 60 | 61 | 同样,移除元素可以通过更新元素之前(元素)的指针指向后一个元素来完成。然而,当涉及多个 writer 时,必须处理相邻元素的并发插入或者删除操作。否则,你可能还会意外地并发地移除新插入的元素,或者撤销了并发移除的元素的移除。 62 | 63 | > 为了保持简单,你可以使用常规的 mutex 来避免并发的修改。这样,读仍然是一个无锁操作,但是你不需要担心处理并发修改。 64 | 65 | 从链表列表中分离元素后,你将遇到与之前相同的问题:它会等待,直到你释放它(或者以其他方式宣称所有权)。在这种情况下,我们之前讨论的基本的 RCU 模式的相同解决方案在这里也有效。 66 | 67 | 总的来说,你可以基于原子指针上的「比较并交换」操作,构建各种精心设计的无锁数据结构,但是你将总是需要一个好的策略来释放或者以其他方式收回分配的所有权。 68 | 69 | 进一步阅读: 70 | 71 | * [维基百科的非阻塞链表](https://en.wikipedia.org/wiki/Non-blocking_linked_list) 72 | * [LWN文章“为链表使用 RCU——案例研究”](https://lwn.net/Articles/610972/) 73 | 74 | ## 基于队列的锁 75 | 76 | (英文版本) 77 | 78 | 对于大多数标准锁定的原语,操作系统内核都会跟踪被阻塞的线程,并负责在被询问时,挑选一个线程来唤醒。一个有趣的替代方案是通过手动地跟踪等待线程的队列来实现 mutex(或者其他锁定原语)。 79 | 80 | 例如一个 mutex 可能作为单个 AtomicPtr 实现,其可以指向一个等待线程(列表)。 81 | 82 | 在这个列表中的每个元素都需要包含一些字段,这些字段用于唤醒相应的线程,例如 `std::thread::Thread` 对象。原子指针一些未使用的位可以用于存储 mutex 自身的状态,以及管理队列状态的任何所需的东西。 83 | 84 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in04.png) 85 | 86 | 有很多可能的变体。队列可能由它自己的锁位保护,或者也可以实现为(部分地)无锁结构。元素不必在堆上分配,而可以是等待的线程的局部变量。队列可以是一个双向链表,不仅包含指向下一个元素的指针,同时也包含指向前一个元素。第一个元素也包含一个指向最后元素的指针,以便有效地在末尾追加一个元素。 87 | 88 | 这种模式仅允许使用可以用于阻塞和唤醒单个线程的方式(例如 `parking`)来实现高效的锁原语。 89 | 90 | Windows SRW 锁([第8章中的“精简的读写(SRW)锁”](./8_Operating_System_Primitives.md#精简的读写srw锁5))使用此模式实现。 91 | 92 | 进一步阅读: 93 | 94 | * [关于 Windows SRW 锁的实现](https://oreil.ly/El8GA) 95 | * [基于队列的锁的 Rust 实现](https://oreil.ly/aFyg1) 96 | 97 | ## 基于阻塞的锁 98 | 99 | (英文版本) 100 | 101 | 为了创建一个尽可能小而高效的 mutex,你可以通过将队列移动到全局的数据结构,在 mutex 自身只留下 1 或者 2 个位,来构建基于队列锁的想法。这样,mutex 仅需要是一个字节。你甚至可以把它放置在一些未使用的指针位中,这允许非常细粒度的锁定,几乎没有其他额外的开销。 102 | 103 | 全局的数据结构可以是一个 `HashMap`,将内存地址映射到等待该地址的 mutex 的线程队列。全局的数据结构通常叫做 `parking lot`,因为它是一组被阻塞(`park`)的线程合集。 104 | 105 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in05.png) 106 | 107 | 这种模式可以是广泛的,其不仅是跟踪 mutex 的队列,同时也还跟踪和其他原语。通过跟踪任何原子变量的队列,这有效地提供了一种不在原生支持该功能的平台上实现类似 futex 功能的方式。 108 | 109 | 这种模式最出名的是 2015 年在 WebKit 中的实现,在那里它被用来锁定 JavaScript 对象。它的实现启发了其他实现,例如流行的 parking_lot Rust crate。 110 | 111 | 进一步阅读: 112 | 113 | * [WebKit 博客,“在 WebKit 中的锁定”](https://oreil.ly/6dPim) 114 | * [parking_lot crate 的文档](https://oreil.ly/UPcXu) 115 | 116 | ## 顺序锁(Sequence Lock) 117 | 118 | (英文版本) 119 | 120 | 顺序锁是不使用传统(阻塞)锁的原子更新(巨大)的数据的另一种解决方案。当数据正在更新时,甚至数据正在准备读取时,它使用一个奇数的原子计数器。 121 | 122 | 在更改数据之前,写入线程必须将计数器从偶数递增到奇数,之后它必须再次递增计数器以使其保持(不同的)偶数值。 123 | 124 | 任何读取线程都可以在任何时候,在不阻塞的情况下,通过在前后读取计数器来读取数据。如果来自计数器的两个值是相等的或是偶数,就没有并发更改,这意味着你读取了有效的数据副本。否则,你可能读取的数据被并发地修改了,在这种情况下,你应该再次尝试。 125 | 126 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_10in06.png) 127 | 128 | 这是一个向其他线程提供数据的绝佳模式,而不会使读线程阻塞写线程。它通常用在操作系统内核和许多嵌入式系统。因为 reader 仅需要对内存的读取访问,并没有涉及指针,因此这可以是一个很好的数据结构,可以在共享内存中安全地使用,在处理器之间,而无需信任 reader。例如,Linux 内核使用这个模式通过为进程提供对(共享)内存的只读访问,非常有效地为进程提供时间戳。 129 | 130 | 一个有趣的问题是,这如何融入内存模型。对相同数据的并发非原子读和写会导致未定义的行为,即使读取数据被忽略。这意味着,从技术上讲,读和写操作都应该仅使用原子操作,尽管整个读或者写并不必须是单一的原子操作。 131 | 132 | ## 教学材料 133 | 134 | (英文版本) 135 | 136 | 花费许多时间(或者许多年)去发明新的并发数据结构和设计人性化的 Rust 实现是非常有趣的。如果你正在寻找与 Rust、原子操作、锁、并发数据结构以及并发性相关的其他知识,那么创建新的教材与其他人分享你的知识也非常有成就感。 137 | 138 | 对于这些主题的初学者,缺乏可接触的资源。Rust 在使系统编程对所有人更易接触方面扮演一个重要的角色,但很多程序员仍然避免底层并发。原子操作通常被认为是一个略微神秘的主题,最后留给一小部分专家,这是可惜的。 139 | 140 | 我希望这本书能够产生显著的影响,但是对于更多的书籍、博客、文章、视频课程、会议演讲和其他关于 Rust 的并发材料,还有很大空间。 141 | 142 | [^1]: 143 | [^2]: 144 | -------------------------------------------------------------------------------- /1_Basic_of_Rust_Concurrency.md: -------------------------------------------------------------------------------- 1 | # 第一章:Rust 并发基础 2 | 3 | (英文版本) 4 | 5 | 早在多核处理器司空见惯之前,操作系统就允许一台计算机运行多个程序。其通过在进程之间反复地切换来完成的,这允许每个进程逐个地逐次取得一点进展。现在,几乎所有的电脑,甚至手机和手表都有着多核处理器,可以真正并行执行多个程序。 6 | 7 | 操作系统尽可能的将进程之间隔离,允许程序完全意识不到其他线程在做什么的情况下做自己的事情。例如,在不先询问操作系统内核的情况下,一个进程通常不能获取其他进程的内存,或者以任意方式与之通信。 8 | 9 | 然而,一个程序可以产生额外的*执行线程*作为*进程*的一部分。同一进程中的线程不会相互隔离。线程共享内存并且可以通过内存相互交互。 10 | 11 | 这一章节将阐述在 Rust 中如何产生线程,并且关于它们的所有基本概念,例如如何安全地在多个线程之间共享数据。本章中解释的概念是本书其余部分的基础。 12 | 13 | > 如果你已经熟悉 Rust 中的这些部分,你可以随时跳过。然而,在你继续下一章节之前,请确保你对线程、内部可变性、Send 和 Sync 有一个好的理解,以及知道什么是互斥锁[^2]、条件变量[^1]以及线程阻塞(park)[^3]。 14 | 15 | ## Rust 中的线程 16 | 17 | (英文版本) 18 | 19 | 每个程序都从一个线程开始:主(main)线程。该线程将执行你的 main 函数,并且如果你需要,可以用它产生更多线程。 20 | 21 | 在 Rust 中,新线程使用来自标准库的 `std::thread::spawn` 函数产生。它接受一个参数:新线程执行的函数。一旦该函数返回,线程就会停止。 22 | 23 | 让我们看一个示例: 24 | 25 | ```rust 26 | use std::thread; 27 | 28 | fn main() { 29 | thread::spawn(f); 30 | thread::spawn(f); 31 | 32 | println!("Hello from the main thread."); 33 | } 34 | 35 | fn f() { 36 | println!("Hello from another thread!"); 37 | 38 | let id = thread::current().id(); 39 | println!("This is my thread id: {id:?}"); 40 | } 41 | ``` 42 | 43 | 我们产生两个线程,它们都将执行 f 作为它们的主函数。这两个线程将输出一个信息并且展示它们的*线程 id*,主线程也将输出它自己的信息。 44 | 45 |
46 |

Thread ID

47 | Rust 标准库为每个线程分配一个唯一的标识符。此标识符可以通过 Thread::id() 访问并且拥有 ThreadId 类型。除了复制 ThreadId 以及检查它们是否相等外,你什么也做不了。不能保证这些 ID 将会连续分配,并且每个线程的 ID 都会有所不同。 48 |
49 | 50 | 如果你运行几次我们上面的示例,你可能注意到输出在运行之间有所不同。一次在机器上特定运行的输出: 51 | 52 | ```txt 53 | Hello from the main thread. 54 | Hello from another thread! 55 | This is my thread id: 56 | ``` 57 | 58 | 惊讶的是,部分输出似乎丢失了。 59 | 60 | 这里发生的情况是:新的线程完成其函数的执行之前,主线程完成了主函数的执行。 61 | 62 | 从主函数返回将退出整个程序,即使其它线程仍然在运行。 63 | 64 | 在这个特定的示例中,在程序被主线程关闭之前,其中一个新的线程只有够到达第二条消息一半的时间。 65 | 66 | 如果我们想要线程在主函数返回之前完成执行,我们可以通过 `join` 它们来等待。为此,我们使用 `spawn` 函数返回的 `JoinHandle`: 67 | 68 | ```rust 69 | fn main() { 70 | let t1 = thread::spawn(f); 71 | let t2 = thread::spawn(f); 72 | 73 | println!("Hello from the main thread."); 74 | 75 | t1.join().unwrap(); 76 | t2.join().unwrap(); 77 | } 78 | ``` 79 | 80 | `.join()` 方法会等待直到线程结束执行并且返回 `std::thread::Result`。如果线程由于 panic 不能成功地完成它的函数,这将包含 panic 消息。我们试图去处理这种情况,或者为 join panic 的线程调用 `.unwrap()` 去 panic。 81 | 82 | 运行我们程序的这个版本,将不再导致输出被截断: 83 | 84 | ```txt 85 | Hello from the main thread. 86 | Hello from another thread! 87 | This is my thread id: ThreadId(3) 88 | Hello from another thread! 89 | This is my thread id: ThreadId(2) 90 | ``` 91 | 92 | 唯一仍然改变的是消息的打印顺序: 93 | 94 | ```txt 95 | Hello from the main thread. 96 | Hello from another thread! 97 | Hello from another thread! 98 | This is my thread id: ThreadId(2) 99 | This is my thread id: ThreadId(3) 100 | ``` 101 | 102 |
103 |

输出锁定

104 | println 宏使用 std::io::Stdout::lock() 去确保输出没有被中断。println!() 表达式将等待直到任意并发的表达式运行完成后,再写入输出。如果不是这样,我们可能得到更多的交错输出: 105 | 106 |
107 |   Hello fromHello from another thread!
108 |   another This is my threthreadHello fromthread id: ThreadId!
109 |   ( the main thread.
110 |   2)This is my thread
111 |   id: ThreadId(3)
112 |
113 | 114 | 与其将函数的名称传递给 `std::thread::spawn`(像我们上面的示例那样),不如传递一个*闭包*。这允许我们捕获并移动值到新的线程: 115 | 116 | ```rust 117 | let numbers = vec![1, 2, 3]; 118 | 119 | thread::spawn(move || { 120 | for n in &numbers { 121 | println!("{n}"); 122 | } 123 | }).join().unwrap(); 124 | ``` 125 | 126 | 在这里,因为我们使用了 `move` 闭包,numbers 的所有权被转移到新产生的线程。如果我们没有使用 `move` 关键字,闭包将会通过引用捕获 numbers。这将导致一个编译错误,因为新的线程可能比变量的生命周期更长。 127 | 128 | 由于线程可能运行直到程序执行结束,因此产生的线程在它的参数类型上有 `'static` 生命周期绑定。换句话说,它只接受永久保留的函数。闭包通过引用捕获局部变量不能够永久保留,因为当局部变量不存在时,引用将变得无效。 129 | 130 | 从线程中取回一个值,是从闭包中返回值来完成的。该返回值可以通过 `join` 方法返回的 `Result` 中获取: 131 | 132 | ```rust 133 | let numbers = Vec::from_iter(0..=1000); 134 | 135 | let t = thread::spawn(move || { 136 | let len = numbers.len(); 137 | let sum = numbers.iter().sum::(); 138 | sum / len // 1 139 | }); 140 | 141 | let average = t.join().unwrap(); // 2 142 | 143 | println!("average: {average}"); 144 | ``` 145 | 146 | 在这里,线程闭包(1)返回的值通过 `join` 方法发送回主线程。 147 | 148 | 如果 numbers 是空的,当它尝试去除以 0 时(2),线程将发生 panic,而 `join` 将会发生 panic 消息,将由于 `unwarp` 导致主线程也 panic。 149 | 150 |
151 |

Thread Builder

152 |

std::thread::spawn 函数事实上仅是 std::thread::Builder::new().spawn().unwrap() 的简写。

153 | 154 |

std::thread::Builder 允许你在产生线程之前为新线程做一些配置。你可以使用它为新线程配置栈大小并给新线程一个名字。线程的名字是可以通过 std::thread::current().name() 获得,这将在 panic 消息中可用,并在监控和大多数调试工具中可见。

155 | 156 |

此外,Builder 的产生函数返回一个 std::io::Result,允许你处理产生新线程失败的情况。如果操作系统内存不足,或者资源限制已经应用于你的程序,这是可能发生的。如果 std::thread::spawn 函数不能去产生一个新线程,它就会 panic。

157 |
158 | 159 | ## 作用域内的线程 160 | 161 | (英文版本) 162 | 163 | 如果我们确信生成的线程不会比某个作用域存活更久,那么线程可以安全地借用那些不会一直存在的东西,例如局部变量,只要它们比该范围活得更久。 164 | 165 | Rust 标准库提供了 `std::thread::scope` 去产生此类*作用域内的线程*。它允许我们产生不超过我们传递给该函数闭包的范围的线程,这使它可能安全地借用局部变量。 166 | 167 | 它的工作原理最好使用一个示例来展示: 168 | 169 | ```rust 170 | let numbers = vec![1, 2, 3]; 171 | 172 | thread::scope(|s| { // 1 173 | s.spawn(|| { // 2 174 | println!("length: {}", numbers.len()); 175 | }); 176 | s.spawn(|| { // 2 177 | for n in &numbers { 178 | println!("{n}"); 179 | } 180 | }); 181 | }); // 3 182 | ``` 183 | 184 | 1. 我们使用闭包调用 `std::thread::scope` 函数。我们的闭包是直接执行,并得到一个参数,`s`,表示作用域。 185 | 2. 我们使用 `s` 去产生线程。该闭包可以借用本地变量,例如 numbers。 186 | 3. 当作用域结束,所有仍没有 join 的线程都会自动 join。 187 | 188 | 这种模式保证了,在作用域产生的线程没有会比作用域更长的生命周期。因此,作用域中的 `spawn` 方法在它的参数类型中没有 `'static` 约束,允许我们去引用任何东西,只要它比作用域有更长的生命周期,例如 numbers。 189 | 190 | 在以上示例中,这两个线程并发地获取 numbers。这是没问题的,因为它们其中的任何一个(或者主线程)都没有修改它。如果我们改变第一个线程去修改 numbers,正如下面展示的,编译器将不允许我们也产生另一个也使用数字的线程: 191 | 192 | ```rust 193 | let mut numbers = vec![1, 2, 3]; 194 | 195 | thread::scope(|s| { 196 | s.spawn(|| { 197 | numbers.push(1); 198 | }); 199 | s.spawn(|| { 200 | numbers.push(2); // 报错! 201 | }); 202 | }); 203 | ``` 204 | 205 | 确切的错误信息取决于 Rust 编译器版本,因为它会在不断改进中以产生更好的诊断,但是试图去编译以上代码将导致以下问题: 206 | 207 | ```txt 208 | error[E0499]: cannot borrow `numbers` as mutable more than once at a time 209 | --> example.rs:7:13 210 | | 211 | 4 | s.spawn(|| { 212 | | -- first mutable borrow occurs here 213 | 5 | numbers.push(1); 214 | | ------- first borrow occurs due to use of `numbers` in closure 215 | | 216 | 7 | s.spawn(|| { 217 | | ^^ second mutable borrow occurs here 218 | 8 | numbers.push(2); 219 | | ------- second borrow occurs due to use of `numbers` in closure 220 | ``` 221 | 222 |
223 |

泄漏启示录

224 |

在 Rust 1.0 之前,标准库有一个函数叫做 std::thread::scoped,它将直接产生一个线程,就像 std::thread::spawn。它允许无 'static 的捕获,因为它返回的不是 JoinHandle,而是当被丢弃时 join 到线程的 JoinGuard。任意的借用数据仅需要比这个 JoinGuard 存活得更久。只要 JoinGuard 在某个时候被丢弃,这似乎就是安全的。

225 | 226 |

就在 Rust 1.0 发布之前,人们慢慢发现它似乎不能保证某些东西一定被丢弃。有很多种方式不能丢弃它,例如创建一个引用计数节点的循环,可以忘记某些东西或者泄漏它。

227 | 228 |

最终,在一些人提及的“泄漏启示录”中得到结论,(安全)接口的设计不能依赖假设对象总是在它们的生命周期结束后丢弃。泄漏一个对象可能会导致泄漏更多对象(例如,泄漏一个 Vec 将也导致泄漏它的元素),但它并不会导致未定义行为(undefind behavior)。因此,std::thread::scoped 将不再视为安全的并从标准库移除。此外,std::mem::forget 从一个不安全的函数升级到安全的函数,以强调忘记(或泄漏)总是一种可能性。

229 | 230 |

直到后来,在 Rust 1.63 中,添加了一个新的 std::thread::scope 功能,其新设计不依赖 Drop 来获得正确性。

231 |
232 | 233 | ## 共享所有权以及引用计数 234 | 235 | (英文版本) 236 | 237 | 目前,我们已经使用了 `move` 闭包([“Rust 中的线程”](#rust-中的线程))将值的所有权转移到线程并从生命周期较长的父线程借用数据([作用域内的线程](#作用域内的线程))。当两个线程之间共享数据,它们之间的任何一个线程都不能保证比另一个线程的生命周期长,那么它们都不能称为该数据的所有者。它们之间共享的任何数据都需要与最长生命周期的线程一样长。 238 | 239 | ### 静态值(static) 240 | 241 | (英文版本) 242 | 243 | 有几种方式去创建不属于单线程的东西。最简单的方式是**静态**值,它由整个程序“拥有”,而不是单个线程。在以下示例中,这两个线程都可以获取 X,但是它们并不拥有它: 244 | 245 | ```rust 246 | static X: [i32; 3] = [1, 2, 3]; 247 | 248 | thread::spawn(|| dbg!(&X)); 249 | thread::spawn(|| dbg!(&X)); 250 | ``` 251 | 252 | 静态值一般由一个常量初始化,它从不会被丢弃,并且甚至在程序的主线程开始之前就已经存在。每个线程都可以借用它,因为可以保证它它总是存在。 253 | 254 | ### 泄漏(Leak) 255 | 256 | (英文版本) 257 | 258 | 另一种方式是通过*泄漏*内存分配的方式共享所有权。使用 `Box::leak`,人们可以释放 `Box` 的所有权,保证永远不会丢弃它。从那时起,`Box` 将永远存在,没有所有者,只要程序运行,任意线程都可以借用它。 259 | 260 | ```rust 261 | let x: &'static [i32; 3] = Box::leak(Box::new([1, 2, 3])); 262 | 263 | thread::spawn(move || dbg!(x)); 264 | thread::spawn(move || dbg!(x)); 265 | ``` 266 | 267 | `move` 闭包可能会让它看起来像我们移动所有权进入线程,但仔细观察 x 的类型就会发现,我们只是给线程一个对数据的*引用*。 268 | 269 | > 引用是 `Copy` 的,这意味着当你“移动”(move)它们的时候,原始内容仍然存在,这就像整数或者布尔内容一样。 270 | 271 | 注意,`'static` 生命周期并不意味着该值自程序开始时就存在,而只是意味着它一直存在到程序的结束。过去并不重要。 272 | 273 | 泄漏 `Box` 的缺点是我们正在泄漏内存。我们获取一些内存,但是从未丢弃和释放它。如果仅发生有限的次数,这就可以了。但是如果我们继续这样做,程序将慢慢地耗尽内存。 274 | 275 | ### 引用计数 276 | 277 | (英文版本) 278 | 279 | 为了确保共享数据能够丢弃和释放内存,我们不能完全放弃它的所有权。相反,我们可以*分享所有权*。通过跟踪所有者的数量,我们确保仅当没有所有者时,才会丢弃该值。 280 | 281 | Rust 标准库通过 `std::rc::Rc` 类型提供了该功能,它是“引用计数”(reference counted)的缩写。它与 `Box` 非常类似,唯一的区别是克隆它将不会分配任何新内存,而是递增存储在包含值旁边的计数器。原始的 `Rc` 和克隆的 `Rc` 将引用相同的内存分配;它们*共享所有权*。 282 | 283 | ```rust 284 | use std::rc::Rc; 285 | 286 | let a = Rc::new([1, 2, 3]); 287 | let b = a.clone(); 288 | 289 | assert_eq!(a.as_ptr(), b.as_ptr()); // 相同内存分配! 290 | ``` 291 | 292 | 丢弃一个 `Rc` 将递减计数。只有最后一个 `Rc`,计数器下降到 0,才会丢弃且释放内存分配中所包含的数据。 293 | 294 | 如果我们尝试去发送一个 Rc 到另一个线程,然而,我们将造成以下的编译错误: 295 | 296 | ```txt 297 | error[E0277]: `Rc` cannot be sent between threads safely 298 | | 299 | 8 | thread::spawn(move || dbg!(b)); 300 | | ^^^^^^^^^^^^^^^ 301 | ``` 302 | 303 | 事实证明,`Rc` 不是*线程安全*的(详见,[线程安全:Send 和 Sync](#线程安全send-和-sync))。如果多个线程有相同内存分配的 `Rc`,那么它们可能尝试并发修改引用计数,这可能产生不可预测的结果。 304 | 305 | 306 | 然而,我们可以使用 `std::sync::Arc`,它代表“原子引用计数”。它与 `Rc` 相同,只是它保证了对引用计数的修改是不可分割的*原子*操作,因此可以安全地与多个线程使用。(详见[第二章](./2_Atomics.md)。) 307 | 308 | ```rust 309 | use std::sync::Arc; 310 | 311 | let a = Arc::new([1, 2, 3]); // 1 312 | let b = a.clone(); // 2 313 | 314 | thread::spawn(move || dbg!(a)); // 3 315 | thread::spawn(move || dbg!(b)); // 3 316 | ``` 317 | 318 | 1. 我们在新的内存分配中放置了一个数组,以及从一开始的引用计数器。 319 | 2. 克隆 Arc 递增引用计数到 2,并为我们提供第二个指向相同内存分配的 Arc。 320 | 3. 两个线程通过各自的 Arc 访问共享的数组。当它们丢弃 Arc 时,两者都会递减引用计数。最后一个丢弃 Arc 的线程将看见计数器递减到 0,并且将丢弃和回收数组的内存。 321 | 322 |
323 |

命名克隆

324 |

如果给每个 Arc 的克隆取一个不同的名称,这可能使得代码变得混乱难以追踪。尽管每个 Arc 的克隆都是一个独立的对象,而给每个克隆赋予不同的名称也并不能很好地反映这一点。

325 | 326 |

Rust 允许(并且鼓励)你通过定义有着新的名称的相同变量去遮蔽变量。如果你在同一作用域这么做,则无法再命名原始变量。但是通过打开一个新的作用域,可以使用类似 let a = a.clone(); 的语句在该作用域内重用相同的名称,同时在作用于外保留原始变量的可用性。

327 | 328 |

通过在新的作用域(使用 {})中封装闭包,我们可以在将变量移动到闭包中之前,进行克隆,而不重新命名它们。

329 |
330 |
331 |
332 | let a = Arc::new([1, 2, 3]);
333 | let b = a.clone();
334 | thread::spawn(move || {
335 |    dbg!(b);
336 | });
337 | dbg!(a);
338 |       
339 | Arc 克隆存活在同一作用域内。每个线程都有自己的克隆,只是名称不同。 340 |
341 |
342 |
343 | let a = Arc::new([1, 2, 3]);
344 | thread::spawn({
345 |     let a = a.clone();
346 |     move || {
347 |         dbg!(a);
348 |     }
349 | });
350 | dbg!(a);
351 |       
352 | Arc 的克隆存活在不同的作用域内。我们可以在每个线程使用相同的名称。 353 |
354 |
355 |
356 | 357 | 因为所有权是共享的,引用计数指针(`Rc` 和 `Arc`)与共享引用(`&T`)有着相同的限制。它们并不能让你对它们包含的值进行可变访问,因为该值在同一时间,可能被其它代码借用。 358 | 359 | 例如,如果我们尝试去排序 `Arc<[i32]>` 中整数的切片,编译器将阻止我们这么做,告诉我们不允许改变数据: 360 | 361 | ```txt 362 | error[E0596]: cannot borrow data in an `Arc` as mutable 363 | | 364 | 6 | a.sort(); 365 | | ^^^^^^^^ 366 | ``` 367 | 368 | ## 借用和数据竞争 369 | 370 | (英文版本) 371 | 372 | 在 Rust 中,可以使用两种方式借用值。 373 | 374 | * *不可变借用* 375 | * 使用 `&` 借用会得到一个*不可变借用*。这样的引用可以被复制。对于它引用访问的数据在所有引用副本之间是共享的。顾名思义,编译器通常不允许你通过这样的引用改变数据,因为那可能会影响当前引用相同数据的其它代码。 376 | * *可变借用* 377 | * 使用 `&mut` 借用会得到一个*可变引用*。可变借用保证了它是该数据的唯一激活的借用。这确保了可变的数据将不会改变任何其它代码正在查看的数据。 378 | 379 | 这两个概念一起,完全阻止了*数据竞争*:一个线程正在改变数据,而另一个线程正在并发地访问数据的情况。数据竞争通常是*未定义行为*[^6],这意味着编译器不需要考虑这些情况。它只是假设它们并不会发生。 380 | 381 | 为了清晰地表达这个意思,让我们来看一看编译器可以使用借用规则作出有用假设的示例: 382 | 383 | ```rust 384 | fn f(a: &i32, b: &mut i32) { 385 | let before = *a; 386 | *b += 1; 387 | let after = *a; 388 | if before != after { 389 | x(); // 从不发生 390 | } 391 | } 392 | ``` 393 | 394 | 这里,我们得到一个整数的不可变引用,并在递增 b 所引用的整数之前和之后存储整数的值。编译器可以自由地假设关于借用和数据竞争的基本规则得到了遵守,这意味着 b 不可能引用与 a 相同的整数。实际上,在对 a 进行引用时,整个程序中没有任何地方对 a 借用的整数进行可变借用。因此,编译器可以轻松地推断 `*a` 不会发生变化,并且 `if` 语句将永远不是 true,并且可以作为优化完全地删除 x 调用。 395 | 396 | 除了使用不安全的块(`unsafe`)禁止一些编译器的安全检查之外,不可能写出打破编译器假设的 Rust 程序。 397 | 398 |
399 |

未定义行为

400 |

类似 C、C++ 和 Rust 都有一套需要遵守的规则,以避免未定义行为。例如,Rust 的规则之一是,对任何对象的可变引用永远不可能超过一个。

401 | 402 |

在 Rust 中,仅当使用 unsafe 代码块才能打破这些规则。“unsafe”并不意味着代码是错误的或者不安全的,而是编译器并没有为你验证你的代码是安全的。如果代码确实违法了这些规则,则称为不健全的(unsound)。

403 | 404 |

允许编译器在不检查的情况下假设这些规则从未破坏。当破坏是,这将导致叫做未定义行为的问题,我们需要不惜一切代价去避免。如果我们允许编译器作出与实际不符的假设,那么它可能很容易导致关于代码不同部分更错误的结论,影响你整个程序。

405 | 406 |

作为一个具体的例子,让我们看看在切片上使用 get_unchecked 方法的小片段:

407 | 408 |
let a = [123, 456, 789];
409 | let b = unsafe { a.get_unchecked(index) };
410 | 411 |

get_unchecked 方法给我们一个给定索引的切片元素,就像 a[index],但是允许编译器假设索引总是在边界,没有任何检查。

412 | 413 |

这意味着,在代码片段中,由于 a 的长度是 3,编译器可能假设索引小于 3。这使我们确保其假设成立。

414 | 415 |

如果我们破坏了这个假设,例如,我们以等于 3 的索引运行,任何事情都可能发生。它可能导致读取 a 之后存储的任何内存内容。这可能导致程序崩溃。它可能会执行程序中完全无关的部分。它可能会引起各种糟糕的情况。

416 | 417 |

或许令人惊讶的是,未定义行为甚至可以“时间回溯”,导致之前的代码出问题。要理解这种情况是如何发生的,想象我们上面的片段有一个 match 语句,如下:

418 | 419 |
match index {
420 |    0 => x(),
421 |    1 => y(),
422 |    _ => z(index),
423 | }
424 | 
425 | let a = [123, 456, 789];
426 | let b = unsafe { a.get_unchecked(index) };
427 | 428 |

由于不安全的代码,编译器被允许假设 index 只有 0、1 或 2。它可能会逻辑的得出结论,我们的 match 语句的最后分支仅会匹配到 2,因此 z 仅会调用为 z(2)。这个结论不仅可以优化匹配,还可以优化 z 本身。这可能包括丢弃代码中未使用的部分。

429 | 430 |

如果我们以 3 为 index 执行此设置,我们的程序可能会尝试执行被优化的部分,导致在我们到达最后一行的 unsafe 块之前就出现不可预测的行为。就像这样,未定义行为通过整个程序向后或者向前传播,而这往往是以非常出乎意料的方式发生。

431 | 432 |

当调用任何的不安全函数时,仔细阅读其文档,确保你完全理解它的安全要求:作为调用者,你需要维持约定或前提条件,以避免未定义行为。

433 |
434 | 435 | ## 内部可变性 436 | 437 | (英文版本) 438 | 439 | 上一节介绍的借用规则可能非常有限——尤其涉及多个线程时。遵循这些规则在线程之间通信极其有限,并且是不可能的,因为多个线程访问的数据都无法改变。 440 | 441 | 幸运的是,有一个逃生方式:内部可变性。有着内部可变性的数据类型略微改变了借用规则。在某些情况下,这些类型可以使用“不可变”的引用进行可变。 442 | 443 | 在[“引用计数”](#引用计数)中,我们已经看到一个设计内部可变性的微妙示例。在 `Rc` 和 `Arc` 都变为引用计数器,即使可能有多个克隆都使用相同的引用计数器。 444 | 445 | 一旦设计内部可变性类型,称“不可变”和“可变”将变得混乱和不准确,因为一些类型可以通过两者变得可变。更准确的称呼是“共享”和“独占”:共享引用(`&T`)可以被复制以及与其它引用共享,然而*独占引用*(`&mut T`)保证了仅有一个对 T 的独占借用。对于大多数类型,共享引用并不允许可变,但有一些例外。由于本书我们将主要处理这些例外情况,我们将在这本书的剩余内容中使用更准确的术语。 446 | 447 | > 请记住,内部可变性仅会影响共享借用的规则,以便在共享时允许可变。它不能改变任意关于独占借用的规则。独占借用仍然保证没有任意激活的借用。导致超过一个活动的独占引用的不安全代码总是涉及未定义行为,不管内部可变性如何。 448 | 449 | 让我们看一看有着内部可变性的一些示例,以及如何通过共享引用允许可变性而不导致未定义行为。 450 | 451 | ### Cell 452 | 453 | (英文版本) 454 | 455 | `std::cell::Cell` 仅是包装了 T,但允许通过共享引用进行可变。为避免未定义行为,它仅允许你将值复制出来(如果 T 实现 Copy)或者将其替换为另一个整体值。此外,它仅用于单个线程。 456 | 457 | 让我们看一看与上一节相似的示例,但是这一次使用 `Cell` 而不是 `i32`: 458 | 459 | ```rust 460 | use std::cell::Cell; 461 | 462 | fn f(a: &Cell, b: &Cell) { 463 | let before = a.get(); 464 | b.set(b.get() + 1); 465 | let after = a.get(); 466 | if before != after { 467 | x(); // 可能发生 468 | } 469 | } 470 | ``` 471 | 472 | 与上次不同,现在 if 条件有可能为真。因为 `Cell` 是内部可变的,只要我们有对它的共享引用,编译器不再假设它的值不再改变。a 和 b 可能引用相同的值,通过 b 也可能影响 a。然而,它可能假设没有其它线程并发获取 cell。 473 | 474 | 对 Cell 的限制并不总是容易处理的。因为它不能直接让我们借用它所持有的值,我们需要将值移动出去(让一些东西替换它的位置),修改它,然后将它放回去,以改变它的内容: 475 | 476 | ```rust 477 | fn f(v: &Cell>) { 478 | let mut v2 = v.take(); // 使用空的 Vec 替换 Cell 中的内容 479 | v2.push(1); 480 | v.set(v2); // 将修改的 Vec 返回 481 | } 482 | ``` 483 | 484 | ### RefCell 485 | 486 | (英文版本) 487 | 488 | 与常规的 Cell 不同的是,`std::cell::RefCell` 允许你以很小的运行时花费,去借用它的内容。`RefCell` 不仅持有 T,同时也持跟踪任何未解除的借用。如果你在已经存在可变借用的情况下尝试借用它(反之亦然),会引发 panic,以避免出现未定义行为。就像 Cell,RefCell 只能在单个线程中使用。 489 | 490 | 借用 RefCell 的内容通过调用 `borrow` 或者 `borrow_mut` 完成: 491 | 492 | ```rust 493 | use std::cell::RefCell; 494 | 495 | fn f(v: &RefCell>) { 496 | v.borrow_mut().push(1); // 我们可以直接修改 `Vec`。 497 | } 498 | ``` 499 | 500 | 尽管 Cell 和 RefCell 有时是非常有用的,但是当我们使用多线程的时候,它们会变得无用。所以让我们继续讨论与并发相关的类型。 501 | 502 | ### 互斥锁和读写锁 503 | 504 | (英文版本) 505 | 506 | *读写锁*(RwLock)[^5]是 `RefCell` 的并发版本。`RwLock` 持有 T 并且跟踪任意未解除的借用。然而,与 RefCell 不同,它在冲突的借用中不会 panic。相反,它会阻塞当前线程——使它进入睡眠——直到冲突的借用消失才会唤醒。在其它线程完成后,我们仅需要耐心的等待轮到我们处理数据。 507 | 508 | 借用 RwLock 的内容称为*锁*。通过锁定它,我们临时阻塞并发的冲突借用,这允许我们没有导致数据竞争的借用它。 509 | 510 | `Mutex`[^4] 与其是非常相似的,但是概念上相对简单的。它不像 RwLock 跟踪共享借用和独占借用的数量,它仅允许独占借用。 511 | 512 | 我们将在[“锁:互斥锁和读写锁”](#锁互斥锁和读写锁)更详细地介绍这些类型。 513 | 514 | ### Atomic 515 | 516 | (英文版本) 517 | 518 | 原子类型表示 Cell 的并发版本,是第 [2](./2_Atomics.md) 章和第 [3](./3_Memory_Ordering.md) 章的主题。与 Cell 相同,它们通过将整个值进行复制来避免未定义行为,而不直接让我们借用内容。 519 | 520 | 与 Cell 不同的是,它们不能是任意大小的。因此,任何 T 都没有通用的 `Atomic` 类型,但仅有特定的原子类型,例如 `AtomicU32` 和 `AtomicPtr`。因为它们需要处理器的支持来避免数据竞争,所以哪些类型可用具体取决于平台。(我们将在[第七章](./7_Understanding_the_Processor.md)研究这个问题。) 521 | 522 | 因为它们的大小非常有限,原子类型通常不直接在线程之间共享所需的信息。相反,它们通常用作工具,使线程之间共享其它(通常是更大的)东西作为可能。当原子用于表示其它数据时,情况可能变得令人意外地复杂。 523 | 524 | ### UnsafeCell 525 | 526 | (英文版本) 527 | 528 | `UnsafeCell` 是内部可变性的原始基石。 529 | 530 | `UnsafeCell` 包装 T,但是没有附带任何条件和限制来避免未定义行为。相反,它的 `get()` 方法仅是给出了它包装值的原始指针,该值仅可以在 `unsafe` 块中使用。它以用户不会导致任何未定义行为的方式使用它。 531 | 532 | 更常见的是,不会直接使用 UnsafeCell,而是将它包装在另一个类型,通过限制接口提供安全,例如 `Cell` 和 `Mutex`。所有有着内部可变性的类型——包括所有以上讨论的类型都建立在 UnsafeCell 之上。 533 | 534 | ## 线程安全:Send 和 Sync 535 | 536 | (英文版本) 537 | 538 | 在这一章节中,我们已经看见一个不是*线程安全*的类型,这些类型仅用于一个单线程,例如 `Rc`、`Cell` 以及其它。由于需要这些限制来避免未定义行为,所以编译器需要理解并为你检查这个限制,这样你就可以在不使用 unsafe 块的情况下使用这些类型。 539 | 540 | 该语言使用两种特殊的 trait 以跟踪这些类型可以安全地用作交叉线程: 541 | 542 | * *Send* 543 | * 一个类型如果可以发送到另一个线程,则其是 `Send` 类型。换句话说,如果一个类型值的所有权可以转移到另一个线程,那么该类型就是 `Send`。例如,`Arc` 是 `Send`,而 `Rc` 不是。 544 | * *Sync* 545 | * 一个类型如果可以共享到另一个线程,则其是 `Sync` 类型。换句话说,当且仅当对该类型(T)的共享引用 `&T` 是 `Send` 的时候,这个类型 T 才是 `Sync`。例如,i32 是 `Sync`,而 `Cell` 就不是。(然而 `Cell` 是 `Send`,但并非 `Sync`。) 546 | 547 | 原始类型,例如 i32、bool 以及 str 都是 `Send` 和 `Sync`。 548 | 549 | 这两个 trait 会自动地为你实现该 trait,这意味着它们会基于各自的字段为你的类型自动地实现。如果结构体的所有字段都实现 `Send` 和 `Sync`,那结构体本身也将实现 `Send` 和 `Sync`。 550 | 551 | 选择退出其中任何一种的方式是去**增加**没有实现该 trait 的字段到你的类型。为此,特殊的 `std::marker::PhantomData` 类型经常派上用场。实际上它在运行时并不存在,它会被被编译器视为 T。它是零开销类型,不占用任何空间。 552 | 553 | 让我们来看看以下的结构体: 554 | 555 | ```rust 556 | use std::marker::PhantomData; 557 | 558 | struct X { 559 | handle: i32, 560 | _not_sync: PhantomData>, 561 | } 562 | ``` 563 | 564 | 在这个示例中,如果 `handle` 是它唯一的字段,`X` 将是 `Send` 和 `Sync`。然而,我们增加一个零开销的 `PhantomData>` 字段,该字段被视为 `Cell<()>`。因为 `Cell<()>` 字段不是 Sync,X 也将不是。但它仍然是 Send,因为所有字段都实现了 Send。 565 | 566 | 原始指针(`*const T` 和 `*mut T`)既不是 Send 也不是 Sync,因为编译器不了解他们表示什么。 567 | 568 | 选择任意 trait 的方式和使用任意其它 trait 相同;使用一个 impl 为你的类型实现 trait: 569 | 570 | ```rust 571 | struct X { 572 | p: *mut i32, 573 | } 574 | 575 | unsafe impl Send for X {} 576 | unsafe impl Sync for X {} 577 | ``` 578 | 579 | 注意,实现这些 trait 需要 `unsafe` 关键字,因为编译器不能为你检查它是否正确。这是你对编译器作出的承诺,你不得不信任它。 580 | 581 | 如果你尝试去移动一些未实现 Send 的值进入另一个线程,编译器将阻止你这样做。用一个小的示例去演示: 582 | 583 | ```rust 584 | fn main() { 585 | let a = Rc::new(123); 586 | thread::spawn(move || { // 报错! 587 | dbg!(a); 588 | }); 589 | } 590 | ``` 591 | 592 | 这里,我们尝试去发送 `Rc` 到一个新线程,但是 `Rc` 与 `Arc` 不同,因为它没有实现 Send。 593 | 594 | 如果我们尝试去编译以上示例,我们将面临一个类似这样的错误: 595 | 596 | ```txt 597 | error[E0277]: `Rc` cannot be sent between threads safely 598 | --> src/main.rs:3:5 599 | | 600 | 3 | thread::spawn(move || { 601 | | ^^^^^^^^^^^^^ `Rc` cannot be sent between threads safely 602 | | 603 | = help: within `[closure]`, the trait `Send` is not implemented for `Rc` 604 | note: required because it's used within this closure 605 | --> src/main.rs:3:19 606 | | 607 | 3 | thread::spawn(move || { 608 | | ^^^^^^^ 609 | note: required by a bound in `spawn` 610 | ``` 611 | 612 | `thread::spawn` 函数需要它的参数实现 Send,并且只有当其所有的捕获都是 Send,闭包才是 Send。如果我们尝试捕获未实现 Send,就会捕捉我们的错误,保护我们避免未定义行为的影响。 613 | 614 | ## 锁:互斥锁和读写锁 615 | 616 | (英文版本) 617 | 618 | 在线程之间共享(可变)数据更常规的有用工具是 `mutex`,它是“互斥”(mutual exclusion)的缩写。mutex 的工作是通过暂时阻塞其它试图同时访问某些数据的线程,来确保线程对某些数据进行独占访问。 619 | 620 | 概念上,mutex 仅有两个状态:解锁和锁定。当线程锁定一个未上锁的 mutex,mutex 被标记为锁定,线程可以立即继续。当线程尝试锁定一个已上锁的 mutex,操作将*阻塞*。当线程等待 mutex 解锁时,其会置入睡眠状态。解锁操作仅能在已上锁的 mutex 上进行,并且应当由锁定它的同一线程完成。如果其它线程正在等待锁定 mutex,解锁将导致唤醒其中一个线程,因此它可以尝试再次锁定 mutex 并且继续它的进程。 621 | 622 | 使用 mutex 保护数据仅是所有线程之间的约定,当它们持有 mutex 锁时,它们才能获取数据。这种方式,没有两个线程可以并发地获取数据和导致数据竞争。 623 | 624 | ### Rust 的互斥锁 625 | 626 | (英文版本) 627 | 628 | Rust 的标准库通过 `std::sync::Mutex` 提供这个功能。它对类型 T 进行泛型化,该类型 T 是 mutex 所保护的数据类型。通过将 T 作为 mutex 的一部分,该数据仅可以通过 mutex 获取,从而提供一个安全的接口,以保证所有线程都遵守这个约定。 629 | 630 | 为确保已上锁的 mutex 仅通过锁定它的线程解锁,所以它没有 `unlock()` 方法。然而,它的 `lock()` 方法返回一个称为 `MutexGuard` 的特殊类型。该 guard 表示保证我们已经锁定 mutex。它通过 `DerefMut` trait 行为表现像一个独占引用,使我们能够独占访问互斥体保护的数据。解锁 mutex 通过丢弃 guard 完成。当我们丢弃 guard 时,我们我们放弃了获取数据的能力,并且 guard 的 `Drop` 实现将解锁 mutex。 631 | 632 | 让我们看一个示例,实践中的 mutex: 633 | 634 | ```rust 635 | use std::sync::Mutex; 636 | 637 | fn main() { 638 | let n = Mutex::new(0); 639 | thread::scope(|s| { 640 | for _ in 0..10 { 641 | s.spawn(|| { 642 | let mut guard = n.lock().unwrap(); 643 | for _ in 0..100 { 644 | *guard += 1; 645 | } 646 | }); 647 | } 648 | }); 649 | assert_eq!(n.into_inner().unwrap(), 1000); 650 | } 651 | ``` 652 | 653 | 在这里,我们有一个 `Mutex`,一个保护整数的 mutex,并且我们启动了十个线程,每个线程会递增这个整数 100 次。每个线程将首先锁定 mutex 去获取 MutexGuard,并且然后使用 guard 去获取整数并修改它。当该变量超出作用域后,guard 会立即隐式丢弃。 654 | 655 | 线程完成后,我们可以通过 `into_inner()` 安全地从整数中移除保护。`into_inner` 方法获取 mutex 的所有权,这确保了没有其它东西可以引用 mutex,从而使 mutex 变得不再必要。 656 | 657 | 尽管递增是逐步的,但是线程仅能够看见 100 的倍数,因为它只能在 mutex 解锁时查看整数。实际上,由于 mutex 的存在,这一百次递增成为了一个单一不可分割的原子操作。 658 | 659 | 为了清晰地看见 mutex 的效果,我们可以让每个线程在解锁 mutex 之前等待一秒: 660 | 661 | ```rust 662 | use std::time::Duration; 663 | 664 | fn main() { 665 | let n = Mutex::new(0); 666 | thread::scope(|s| { 667 | for _ in 0..10 { 668 | s.spawn(|| { 669 | let mut guard = n.lock().unwrap(); 670 | for _ in 0..100 { 671 | *guard += 1; 672 | } 673 | thread::sleep(Duration::from_secs(1)); // 新增! 674 | }); 675 | } 676 | }); 677 | assert_eq!(n.into_inner().unwrap(), 1000); 678 | } 679 | ``` 680 | 681 | 当你现在运行程序,你将看见大约需要花费 10s 才能完成。每个线程仅等待 1s,但是 mutex 确保一次仅有一个线程这么做。 682 | 683 | 如果我们在睡眠 1s 之前丢弃 guard,并且因此解锁 mutex,我们将看到并行发生: 684 | 685 | ```rust 686 | fn main() { 687 | let n = Mutex::new(0); 688 | thread::scope(|s| { 689 | for _ in 0..10 { 690 | s.spawn(|| { 691 | let mut guard = n.lock().unwrap(); 692 | for _ in 0..100 { 693 | *guard += 1; 694 | } 695 | drop(guard); // 新增:在睡眠之前丢弃 guard! 696 | thread::sleep(Duration::from_secs(1)); 697 | }); 698 | } 699 | }); 700 | assert_eq!(n.into_inner().unwrap(), 1000); 701 | } 702 | ``` 703 | 704 | 有了这些变化,这个程序大约仅需要 1s,因为 10 个线程现在可以同时执行 1s 的睡眠。这表明了 mutex 锁定时间保持尽可能短的重要性。将 mutex 锁定时间超过必要时间可能会完全抵消并行带来的好处,实际上会强制所有操作按顺序执行。 705 | 706 | ### 锁中毒(poison) 707 | 708 | (英文版本) 709 | 710 | 上述示例中 `unwarp()` 调用和*锁中毒*有关。 711 | 712 | 当线程在持有锁时 panic,Rust 中的 mutex 将被标记为*中毒*。当这种情况发生时,Mutex 将不再被锁定,但调用它的 `lock` 方法将导致 `Err`,以表明它已经中毒。 713 | 714 | 这是一个防止由 mutex 保护的数据处于不一致状态的机制。在我们上面的示例中,如果一个线程在整数递增到 100 之前崩溃,mutex 将解锁并且整数将处于一个意外的状态,它不再是 100 的倍数,这可能打破其它线程的假设。在这种情况下,自动标记 mutex 中毒,强制用户处理这种可能。 715 | 716 | 在中毒的 mutex 上调用 `lock()` 仍然可能锁定 mutex。由 `lock()` 返回的 Err 包含 `MutexGuard`,允许我们在必要时纠正不一致的状态。 717 | 718 | 虽然锁中毒是一种强大的机制,在实践中,从潜在的不一致状态恢复并不常见。如果锁中毒,大多数代码要么忽略了中毒或者使用 `unwrap()` 去 panic,这有效地将 panic 传递给使用 mutex 的所有用户。 719 | 720 |
721 |

MutexGuard 的生命周期

722 |

尽管隐式丢弃 guard 解锁 mutex 很方便,但是它有时会导致微妙的意外。如果我们使用 let 语句授任 guard 一个名字(正如我们上面的示例),看它什么时候会被丢弃相对简单,因为局部变量定义在它们作用域的末尾。然而,正如上述示例所示,不明确地丢弃 guard 可能导致 mutex 锁定的时间超过所需时间。

723 | 724 |

在不给它指定名称的情况下使用 guard 也是可能的,并且有时非常方便。因为 MutexGuard 保护数据的行为像独占引用,我们可以直接使用它,而无需首先为他授任一个名称。例如,你有一个 Mutex<Vec<i32>>,你可以在单个语句中锁定 mutex,将项推入 Vec,并且再次锁定 mutex:

725 | 726 |
list.lock().unwrap().push(1);
727 | 728 |

任何更大表达式产生的临时值,例如通过 lock() 返回的 guard,将在语句结束后被丢弃。尽管这似乎显而易见,但它导致了一个常见的问题,这通常涉及 matchif let 以及 while let 语句。以下是遇到该陷阱的示例:

729 | 730 |
if let Some(item) = list.lock().unwrap().pop() {
731 |     process_item(item);
732 | }
733 | 734 |

如果我们的旨意就是锁定 list、弹出 item、解锁 list 然后在解锁 list 后处理 item,我们在这里犯了一个微妙而严重的错误。临时的 guard 直到完整的 if let 语句结束后才能被丢弃,这意味着我们在处理 item 时不必要地持有锁。

735 | 736 | 或许,意外的是,对于类似的 if 语句,这并不会发生,例如以下示例: 737 | 738 |
if list.lock().unwrap().pop() == Some(1) {
739 |     do_something();
740 | }
741 | 742 |

在这里,临时的 guard 在 if 语句的主体执行之前就已经丢弃了。该原因是,通常 if 语句的条件总是一个布尔值,它并不能借用任何东西。没有理由将临时的生命周期从条件开始延长到语句的结尾。对于 if let 语句,情况可能并非如此。例如,如果我们使用 front(),而不是 pop(),项将会从 list 中借用,因此有必要保持 guard 存在。因为借用检查实际上只是一种检查,它并不会影响何时以及什么顺序丢弃,所以即使我们使用了 pop(),情况仍然是相同的,尽管那并不是必须的。

743 | 744 |

我们可以通过将弹出操作移动到单独的 let 语句来避免这种情况。然后在该语句的末尾放下 guard,在 if let 之前:

745 | 746 |
let item = list.lock().unwrap().pop();
747 | if let Some(item) = item {
748 |     process_item(item);
749 | }
750 |
751 | 752 | ### 读写锁 753 | 754 | (英文版本) 755 | 756 | 互斥锁仅涉及独占访问。MutexGuard 将提供受保护数据的一个独占引用(`&mut T`),即使我们仅想要查看数据,并且共享引用(`&T`)就足够了。 757 | 758 | 读写锁是一个略微更复杂的 mutex 版本,它能够区分独占访问和共享访问的区别,并且可以提供两种访问方式。它有三种状态:解锁、由单个 *writer* 锁定(用于独占访问)以及由任意数量的 reader 锁定(用于共享访问)。它通常用于通常由多个线程读取的数据,但只是偶尔一次。 759 | 760 | Rust 标准库通过 `std::sync::RwLock` 类型提供该锁。它与标准库的 Mutex 工作类似,只是它的接口大多是分成两个部分。然而,单个 `lock()` 方法,它有 `read()` 和 `write()` 方法,用于为 reader 或 writer 进行锁定。它还附带了两种守卫类型,一种用于 reader,一种用于 writer:RwLockReadGuard 和 RwLockWriteGuard。前者只实现了 Deref,其行为像受保护数据共享引用,后者还实现了 DerefMut,其行为像独占引用。 761 | 762 | 它实际上是 `RefCell` 的多线程版本,动态地跟踪引用的数量以确保借用规则得到维护。 763 | 764 | `Mutex` 和 `RwLock` 都需要 T 是 Send,因为它们可能发送 T 到另一个线程。除此之外,`RwLock` 也需要 T 实现 Sync,因为它允许多个线程对受保护的数据持有共享引用(`&T`)。(严格地说,你可以创建一个并没有实现这些需求 T 的锁,但是你不能在线程之间共享它,因为锁本身并没有实现 Sync)。 765 | 766 | Rust 标准库仅提供一种通用的 `RwLock` 类型,但它的实现依赖于操作系统。读写锁之间有很多细微差别。当有 writer 等待时,即使当锁已经读锁定的,很多实现将阻塞新的 reader。这样做是为了防止 *writer 挨饿*,在这种情况下,很多 reader 将集体阻止锁解锁,从而不允许任何 writer 更新数据。 767 | 768 |
769 |

在其他语言中的互斥锁

770 |

Rust 标准的 Mutex 和 RwLock 类型与你在其它语言(例如 C、C++)发现的看起来有一点不同。

771 | 772 |

最大的区别是,Rust 的 Mutex<T> 数据包含它正在保护的数据。例如,在 C++ 中,std::mutex 并不包含着它保护的数据,甚至不知道它在保护什么。这意味着,用户有职责记住哪些数据由 mutex 保护,并且确保每次访问“受保护”的数据都锁定正确的 mutex。注意,当阅读其它语言涉及到 mutex 的代码,或者与不熟悉 Rust 程序员沟通时,非常有用。Rust 程序员可能讨论关于“数据在 mutex 之中”,或者说“mutex 中包装数据”这类话,这可能让只熟悉其它语言 mutex 的程序员感到困惑。

773 | 774 |

如果你真的需要一个不包含任何内容的独立 mutex,例如,保护一些外部硬件,你可以使用 Mutex<()>。但即使是这种情况,你最好定义一个(可能 0 大小开销)的类型来与该硬件对接,并将其包装在 Mutex 之中。这样,在与硬件交互之前,你仍然可以强制锁定 mutex。

775 |
776 | 777 | ## 等待: 阻塞(Park)和条件变量 778 | 779 | (英文版本) 780 | 781 | 当数据由多个线程更改时,在许多情况下,它们需要等待一些事件,以便关于数据的某些条件变为真。例如,如果我们有一个 mutex 保护的 Vec,我们可能想要等待直到它包含任何东西。 782 | 783 | 尽管 mutex 允许线程等待直到它解锁,但它不提供等待任何其它条件的功能。如果我们只拥有一个 mutex,我们不得不持有锁定的 mutex,以反复检查 Vec 中是否有任意东西。 784 | 785 | ### 线程阻塞 786 | 787 | (英文版本) 788 | 789 | 一种方式是去等待来自另一个线程的通知,其被称为*线程阻塞*。一个线程可以阻塞它自己,将它置入睡眠状态,阻止它消耗任意 CPU 周期。然后,另一个线程可以解锁阻塞的线程,将其从睡眠中唤醒。 790 | 791 | 线程阻塞可以通过 `std::thread::park()` 函数获得。对于解锁,你可以在 `Thread` 对象中调用 `unpark()` 函数表示你想要解锁该线程。这样的对象可以通过 spawn 返回的 join 句柄获得,或者也可以通过 `std::thread::current()` 从线程本身中获得。 792 | 793 | 让我们深入研究在线程之间使用 mutex 共享队列的示例。在以下示例中,一个新产生的线程将消费来自队列的项,尽管主线程将每秒插入新的项到队列。线程阻塞被用于在队列为空时使消费线程等待。 794 | 795 | ```rust 796 | use std::collections::VecDeque; 797 | 798 | fn main() { 799 | let queue = Mutex::new(VecDeque::new()); 800 | 801 | thread::scope(|s| { 802 | // 消费线程 803 | let t = s.spawn(|| loop { 804 | let item = queue.lock().unwrap().pop_front(); 805 | if let Some(item) = item { 806 | dbg!(item); 807 | } else { 808 | thread::park(); 809 | } 810 | }); 811 | 812 | // 产生线程 813 | for i in 0.. { 814 | queue.lock().unwrap().push_back(i); 815 | t.thread().unpark(); 816 | thread::sleep(Duration::from_secs(1)); 817 | } 818 | }); 819 | } 820 | ``` 821 | 822 | 消费线程运行一个无限循环,它将项弹出队列,使用 `dbg` 宏展示它们。当队列为空的时候,它停止并且使用 `park()` 函数进行睡眠。如果它得到解锁,`park()` 调用将返回,循环继续,再次从队列中弹出项,直到它是空的。等等。 823 | 824 | 生产线程将其推入队列,每秒产生一个新的数字。每次递增一个项时,它都会在 Thread 对象上使用 `unpark()` 方法,该方法引用消费线程来解锁它。这样,消费线程就会被唤醒处理新的元素。 825 | 826 | 需要注意的一点是,即使我们移除**阻塞**,这个程序在理论上仍然是正确的,尽管效率低下。这是重要的,因为 `park()` 不能保证它将由于匹配 `unpark()` 而返回。尽管有些罕见,但它很可能会有*虚假唤醒*。我们的示例处理得很好,因为消费线程会锁定队列,可以看到它是空的,然后直接解锁它并再次阻塞。 827 | 828 | 线程阻塞的一个重要属性是,在线程自己进入阻塞之前,对 `unpark()` 的调用不会丢失。对 unpark 的请求仍然被记录下来,并且下次线程尝试挂起自己的时候,它会清除该请求并且直接继续执行,实际上并不会进入睡眠状态。为了理解这对于正确操作的关键性,让我们来看一下程序可能执行步骤的顺序: 829 | 830 | 1. 消费线程(让我们称之为 C)锁定队列。 831 | 2. C 尝试去从队列中弹出项,但是它是空的,导致 None。 832 | 3. C 解锁队列。 833 | 4. 生产线程(我们将称为 P)锁定队列。 834 | 5. P 推入一个新的项进入队列。 835 | 6. P 再次解锁队列。 836 | 7. P 调用 `unpark()` 去通知 C,有一些新的项。 837 | 8. C 调用 `park()` 去睡眠,以等待更多的项。 838 | 839 | 虽然在步骤 3 解锁队列和在步骤 8 阻塞之间很可能仅有一个很短的时间,但第 4 步和第 7 步可能在线程阻塞自己之前发生。如果 `unpark()` 在线程没有挂起时不执行任何操作,那么通知将会丢失。即使队列中有项,消费线程仍然在等待。由于 `unpark()` 请求被保存,以供将来调用 `park()` 时使用,我们不必担心这个问题。 840 | 841 | 然而,unpark 请求并不会堆起来。先调用两次 `unpark()`,然后再调用两次 `park()`,线程仍然会进入睡眠状态。第一次 `park()` 清除请求并直接返回,但第二次调用通常让它进入睡眠。 842 | 843 | 这意味着,在我们上面的示例中,重要的是我们看见队列为空的时候,我们仅会阻塞线程,而不是在处理每个项之后将其阻塞。然而由于巨长的(1s)睡眠,这种情况在本示例中几乎不可能发生,但多个 `unpark()` 调用仅能唤醒单个 `park()` 调用。 844 | 845 | 不幸的是,这确实意味着,如果在 `park()` 返回后,立即调用 `unpark()`,但是在队列得到锁定并清空之前,`unpark()` 调用是不必要的,但仍然会导致下一个 `park()` 调用立即返回。这导致(空的)队列多次被锁定并解锁。虽然这不会影响程序的正确性,但这确实会影响它的效率和性能。 846 | 847 | 这种机制在简单的情况下是好的,比如我们的示例,但是当东西变得复杂,情况可能会很糟糕。例如,如果我们有多个消费线程从相同的队列获取项时,生产线程将不会知道有哪些消费者实际上在等待以及应该被唤醒。生产者将必须知道消费者正在等待的时间以及正在等待的条件。 848 | 849 | ### 条件变量 850 | 851 | (英文版本) 852 | 853 | 条件变量是一个更通用的选项,用于等待受 mutex 保护的数据发生变化。它有两种基本操作:等待和通知。线程可以在条件变量上等待,然后在另一个线程通知相同条件变量时被唤醒。多个线程可以在同样的条件变量上等待,通知可以发送给一个等待线程或者所有等待线程。 854 | 855 | 这意味着我们可以为我们感兴趣的事件或条件创建一个条件变量,例如,队列是非空的,并且在该条件下等待。任意导致事件或条件发生的线程都会通知条件变量,无需知道哪个或有多个线程对该通知感兴趣。 856 | 857 | 为了避免在解锁 mutex 和等待条件变量的短暂时间失去通知的问题,条件变量提供了一种*原子地*解锁 mutex 和开始等待的方式。这意味着根本没有通知丢失的时刻。 858 | 859 | Rust 标准库提供了 `std::sync::Condvar` 作为条件变量。它的等待方法接收 `MutexGuard`,以保证我们已经锁定 mutex。它首先解锁 mutex 并进入睡眠。稍后,当唤醒时,它重新锁定 mutex 并且返回一个新的 MutexGuard(这证明了 mutex 再次被锁定)。 860 | 861 | 它有两个通知方法:`notify_one` 仅唤醒一个线程(如果有),和 `notify_all` 去唤醒所有线程。 862 | 863 | 让我们改用 Condvar 修改我们用于线程阻塞的示例: 864 | 865 | ```rust 866 | use std::sync::Condvar; 867 | 868 | let queue = Mutex::new(VecDeque::new()); 869 | let not_empty = Condvar::new(); 870 | 871 | thread::scope(|s| { 872 | s.spawn(|| { 873 | loop { 874 | let mut q = queue.lock().unwrap(); 875 | let item = loop { 876 | if let Some(item) = q.pop_front() { 877 | break item; 878 | } else { 879 | q = not_empty.wait(q).unwrap(); 880 | } 881 | }; 882 | drop(q); 883 | dbg!(item); 884 | } 885 | }); 886 | 887 | for i in 0.. { 888 | queue.lock().unwrap().push_back(i); 889 | not_empty.notify_one(); 890 | thread::sleep(Duration::from_secs(1)); 891 | } 892 | }); 893 | ``` 894 | 895 | * 我们必须改变一些事情: 896 | * 我们现在不仅有一个包含队列的 Mutex,同时有一个 Condvar 去通信“不为空”的条件。 897 | * 我们不再需要知道要唤醒哪个线程,因此我们不再存储 spawn 的返回值。而是,我们通过使用 `notify_one` 方法的条件变量通知消费者。 898 | * 解锁、等待以及重新锁定都是通过 `wait` 方法完成的。我们不得不稍微重组控制流,以便传递 guard 到 wait 方法,同时在处理项之前仍然丢弃它。 899 | 900 | 现在,我们可以根据自己的需求生成尽可能多的消费线程,甚至稍后生成更多线程,而无需更改任何东西。条件变量会负责将通知传递给任何感兴趣的线程。 901 | 902 | 如果我们有个更加复杂的系统,其线程对不同条件感兴趣,我们可以为每个条件定义一个 `Condvar`。例如,我们能定义一个来指示队列是非空的条件,并且另一个指示队列是空的条件。然后,每个线程将等待与它们正在做的事情相关的条件。 903 | 904 | 通常,Condvar 仅能与单个 Mutex 一起使用。如果两个线程尝试使用两个不同的 mutex 去并发地等待条件变量,它可能导致 panic。 905 | 906 | Condvar 的缺点是,它仅能与 Mutex 一起工作,对于大多数用例是没问题的,因为已经在保护数据时使用了 mutex。 907 | 908 | `thread::park()` 和 `Condvar::wait()` 也都有一个有时间限制的变体:`thread::park_timeout()` 和 `Condvar::wait_timeout()`。它们接受一个额外的参数 Duration,表示在多长时间后放弃等待通知并无条件地唤醒。 909 | 910 | ## 总结 911 | 912 | (英文版本) 913 | 914 | * 多线程可以并发地运行在相同程序并且可以在任意时间生成。 915 | * 当主线程结束,主程序结束。 916 | * 数据竞争是未定义行为,它会由 Rust 的类型系统完全地阻止(在安全的代码中)。 917 | * 常规的线程可以像程序运行一样长时间,并且因此只能借用 `'static` 数据。例如静态值和泄漏内存分配。 918 | * 引用计数(Arc)可以用于共享所有权,以确保只要有一个线程使用它,数据就会存在。 919 | * 作用域线程用于限制线程的生命周期,以允许其借用非 `'static` 数据,例如作用域变量。 920 | * `&T` 是*共享引用*。`&mut T` 是*独占引用*。常规类型不允许通过共享引用可变。 921 | * 一些类型有着内部可变性,这要归功于 `UnsafeCell`,它允许通过共享引用改变。 922 | * Cell 和 RefCell 是单线程内部可变性的标准类型。Atomic、Mutex 以及 RwLock 是它们多线程等价物。 923 | * Cell 和原子类型仅允许作为整体替换值,而 RefCell、Mutex 和 RwLock 允许你通过动态执行访问规则直接替换值。 924 | * 线程阻塞可以是等待某种条件的便捷方式。 925 | * 当条件是关于由 Mutex 保护的数据时,使用 `Condvar` 时更方便,并且比线程阻塞更有效。 926 | 927 |

928 | 下一篇,第二章:Atomic 929 |

930 | 931 | [^1]: 932 | [^2]: 933 | [^3]: 934 | [^4]: 935 | [^5]: 936 | [^6]: 937 | -------------------------------------------------------------------------------- /2_Atomics.md: -------------------------------------------------------------------------------- 1 | # 第二章:Atomic 2 | 3 | (英文版本) 4 | 5 | *原子*(atomic)这个单词来自于希腊语 `ἄτομος`,意味着不可分割的,不能被切割成更小的块。在计算机科学中,它被用于描述一个不可分割的操作:它要么完全完成,要么还没发生。 6 | 7 | 正如在[第一章“借用和数据竞争”](./1_Basic_of_Rust_Concurrency.md#借用和数据竞争)中提及的,多线程并发地读取和修改相同的变量会导致未定义行为。然而,原子操作确实允许不同线程去安全地读取和修改相同的变量。因为该操作是不可分割的,它要么完全地在一个操作之前完成,要么在另一个操作之后完成,从而避免未定义行为。稍后,在[第七章](./7_Understanding_the_Processor.md),我们将在硬件层面查看它们是如何工作的。 8 | 9 | 原子操作是任何涉及多线程的主要基石。所有其它的并发原语,例如互斥锁,条件变量都使用原子操作实现。 10 | 11 | 在 Rust 中,原子操作可以作为 `std::sync::atomic` 标准原子类型的方法使用。它们的名称都以 Atomic 开头,例如 AtomicI32 或 AtomicUsize。可用的原子类型取决于硬件架构和一些操作系统,但几乎所有的平台都提供了指针大小的所有原子类型。 12 | 13 | 与大多数类型不同,它们允许通过共享引用进行修改(例如,`&AtomicU8`)。正如[第一章“内部可变性”](./1_Basic_of_Rust_Concurrency.md#内部可变性)讨论的那样,这都要归功于它。 14 | 15 | 每一个可用的原子类型都有着相同的接口,包括存储(store)和加载(load)、原子“获取并修改(fetch-and-modify)”操作方法、和一些更高级的“比较并交换”(compare-and-exchange)[^4]方法。我们将在这章节的后半部分讨论它们。 16 | 17 | 但是,在我们研究不同原子操作之前,我们需要简要谈谈叫做*内存排序*[^1]的概念: 18 | 19 | 每一个原子操作都接收 `std::sync::atomic::Ordering` 类型的参数,这决定了我们对操作相对排序的保证。保证最少的简单变体是 `Relaxed`。`Relaxed` 只保证在单个原子变量中的一致性,但是在不同变量的相对操作顺序没有任何保证。 20 | 21 | 这意味着两个线程可能看到不同变量的操作以不同的顺序下发生。例如,如果一个线程首先写入一个变量,然后非常快速的写入第二个变量,另一个线程可能看见这以相反的顺序发生。 22 | 23 | 在这章节,我们将仅关注不会出现这种问题的使用情况,并且在所有地方都简单地使用 `Relaxed`,而不深入讨论更多细节。我们将在[第三章](./3_Memory_Ordering.md)讨论内存排序的所有细节以及其它可用内存排序。 24 | 25 | ## Atomic 的加载和存储操作 26 | 27 | (英文版本) 28 | 29 | 我们将查看的前两个原子操作是最基本的:load 和 store。它们的函数签名如下,使用 AtomicI32 作为示例: 30 | 31 | ```rust 32 | impl AtomicI32 { 33 | pub fn load(&self, ordering: Ordering) -> i32; 34 | pub fn store(&self, value: i32, ordering: Ordering); 35 | } 36 | ``` 37 | 38 | load 方法以原子方式加载存储在原子变量中的值,并且 store 方法原子方式存储新值。注意,store 方法采用共享引用(`&T`),而不是独占引用(`&mut T`),即使它修改了值。 39 | 40 | 让我们来看看这两种方式的使用示例。 41 | 42 | ### 示例:停止标识 43 | 44 | (英文版本) 45 | 46 | 第一个示例使用 AtomicBool 作为*停止标识*。这个标识被用于告诉其它线程去停止运行: 47 | 48 | ```rust 49 | use std::sync::atomic::AtomicBool; 50 | use std::sync::atomic::Ordering::Relaxed; 51 | 52 | fn main() { 53 | static STOP: AtomicBool = AtomicBool::new(false); 54 | 55 | // 产生一个线程,去做一些工作。 56 | let background_thread = thread::spawn(|| { 57 | while !STOP.load(Relaxed) { 58 | some_work(); 59 | } 60 | }); 61 | 62 | // 使用主线程监听用户的输入。 63 | for line in std::io::stdin().lines() { 64 | match line.unwrap().as_str() { 65 | "help" => println!("commands: help, stop"), 66 | "stop" => break, 67 | cmd => println!("unknown command: {cmd:?}"), 68 | } 69 | } 70 | 71 | // 告知后台线程需要停止。 72 | STOP.store(true, Relaxed); 73 | 74 | // 等待直到后台线程完成。 75 | background_thread.join().unwrap(); 76 | } 77 | ``` 78 | 79 | 在本示例中,后台线程反复地运行 `some_work()`,而主线程允许用户输入一些命令与其它线程交互程序。在这个示例中,唯一有用的命令是 `stop`,来使程序停止。 80 | 81 | 为了使后台线程停止,原子 **STOP** 布尔值使用此条件与后台线程交互。当前台线程读取到 `stop` 命令,它设置标识到 true,在每次新迭代之前,后台线程都会检查。主线程等待直到后台线程使用 join 方法完成它当前的迭代。 82 | 83 | 只要后台线程定期检查标识,这个简单的工作方案就是好的。如果它在 `some_work()` 卡住很长时间,这可能在 stop 命令和程序退出之间出现不可接受的延迟。 84 | 85 | ### 示例:进度报道 86 | 87 | (英文版本) 88 | 89 | 在我们的下一个示例中,我们通过后台线程逐步地处理 100 个元素,而主线程为用户提供定期地更新: 90 | 91 | ```rust 92 | use std::sync::atomic::AtomicUsize; 93 | 94 | fn main() { 95 | let num_done = AtomicUsize::new(0); 96 | 97 | thread::scope(|s| { 98 | // 一个后台线程,去处理所有 100 个元素。 99 | s.spawn(|| { 100 | for i in 0..100 { 101 | process_item(i); // 假设该处理需要一些时间。 102 | num_done.store(i + 1, Relaxed); 103 | } 104 | }); 105 | 106 | // 主线程没秒展示一次状态更新。 107 | loop { 108 | let n = num_done.load(Relaxed); 109 | if n == 100 { break; } 110 | println!("Working.. {n}/100 done"); 111 | thread::sleep(Duration::from_secs(1)); 112 | } 113 | }); 114 | 115 | println!("Done!"); 116 | } 117 | ``` 118 | 119 | 这次,我们使用一个[作用域内的线程](./1_Basic_of_Rust_Concurrency.md#作用域内的线程),它将自动地为我们处理线程的 join,并且也允许我们借用局部变量。 120 | 121 | 每次后台线程完成处理元素时,它都会将处理的元素数量存储在 AtomicUsize 中。与此同时,主线程向用户显示该数字,告知该进度,大约每秒一次。一旦主线程看见所有 100 个元素已经被处理,它就会退出作用域,它会隐式地 join 后台线程,并且告知用户所有都完成。 122 | 123 | #### 同步 124 | 125 | (英文版本) 126 | 127 | 一旦最后一个元素处理完成,主线程可能需要整整一秒才知道,这在最后引入了不必要的延迟。为了解决这个问题,我们在每当有新的消息有用时,使用线程阻塞([第一章“线程阻塞”](./1_Basic_of_Rust_Concurrency.md#线程阻塞))去唤醒睡眠中的主线程。 128 | 129 | 以下是相同的示例,但是现在使用 `thread::park_timeout` 而不是 `thread::sleep`: 130 | 131 | ```rust 132 | fn main() { 133 | let num_done = AtomicUsize::new(0); 134 | 135 | let main_thread = thread::current(); 136 | 137 | thread::scope(|s| { 138 | // // 一个后台线程,去处理所有 100 个元素。 139 | s.spawn(|| { 140 | for i in 0..100 { 141 | process_item(i); // 假设该处理需要一些时间。 142 | num_done.store(i + 1, Relaxed); 143 | main_thread.unpark(); // 唤醒主线程 144 | } 145 | }); 146 | 147 | // 主线程展示的状态更新 148 | loop { 149 | let n = num_done.load(Relaxed); 150 | if n == 100 { break; } 151 | println!("Working.. {n}/100 done"); 152 | thread::park_timeout(Duration::from_secs(1)); 153 | } 154 | }); 155 | 156 | println!("Done!"); 157 | } 158 | ``` 159 | 160 | 没有什么变化,我们通过 `thread::current()` 获取主线程的句柄,该句柄现在被用于在每次后台线程状态更新后,阻塞主线程。主线程现在使用 park_timeout 而不是 sleep,这样它就可以被中断。 161 | 162 | 现在,任何状态更新都会立即告知用户,同时仍然每秒重复上一次更新,以展示程序仍然在运行。 163 | 164 | ### 示例:惰性初始化 165 | 166 | (英文版本) 167 | 168 | 在移动到更高级的原子操作之前,我们来看最后一个关于*惰性初始化*的示例。 169 | 170 | 想象有一个值 x,我们可以从一个文件读取它、从操作系统获取或者以其他方式计算得到,我们期待去在程序运行期间它是一个常量。或许 x 是操作系统的版本、内存的总数或者 tau 的第 400 位。对于这个示例,这不重要。 171 | 172 | 因为我们不期待它去发生变化,我们仅在第一次请求或计算时需要它,并且记住它的结果。需要它的第一个线程必须计算值,但它可以存储它到一个 `static` 的原子中,使所有线程可用,包括稍后的自己。 173 | 174 | 让我们看看这个示例。为了使这些简单,我们将假设 x 永远不会是 0,这样我们就可以在计算之前使用 0 作为占位符。 175 | 176 | ```rust 177 | use std::sync::atomic::AtomicU64; 178 | 179 | fn get_x() -> u64 { 180 | static X: AtomicU64 = AtomicU64::new(0); 181 | let mut x = X.load(Relaxed); 182 | if x == 0 { 183 | x = calculate_x(); 184 | X.store(x, Relaxed); 185 | } 186 | x 187 | } 188 | ``` 189 | 190 | 第一个线程调用 `get_x()` 将检查 `static X` 并看见它仍然是 0,计算它的值并且存回结果到 static X 中,使它在未来可用。稍后,任意对 `get_x()` 的调用都将看见静态值不是 0,并立即返回,没有立即计算。 191 | 192 | 然而,如果第二个线程调用 `get_x()`,而第一个线程仍然正在计算 x,第二个线程将也看见 0,并且也在并行的计算 x。其中一个线程将最后复写另一个线程的结果,这取决于哪一个线程先完成。这被叫做*竞争*。并不是数据竞争(这是一个未定义行为),在 Rust 中不使用 unsafe 的情况下是不可能发生的,但这仍然有一个不可预测的赢者的竞争。 193 | 194 | 因为我们期待 x 是常量,那么谁赢得比赛并不重要,因为无论如何结果都是一样。依赖于 `calculate_x()` 会花费多少时间,这可能非常好或者很糟糕。 195 | 196 | 如果 `calculate_x()` 预计花费很长时间,则最好在第一个线程仍在初始化 X 时等待,以避免不必要的浪费处理器时间。你可以使用一个条件变量或者线程阻塞([第一章“等待-阻塞和条件变量”](./1_Basic_of_Rust_Concurrency.md#等待-阻塞park和条件变量))来实现这个,但是对于一个小例子来说,这很快将变得复杂。Rust 标准库通过 `std::sync::Once` 和 `std::sync::OnceLock` 提供了此功能,所以通常这些不需要由你自己实现。 197 | 198 | ## 获取并修改操作 199 | 200 | (英文版本) 201 | 202 | 注意,我们已经看见基础 load 和 store 操作的一些用例,让我们继续更有趣的操作:*获取并修改*(fetch-and-modify)操作。这些操作修改原子变量,但也加载(获取)原始值,作为一个单原子操作。 203 | 204 | 最常用的是 `fetch_add` 和 `fetch_sub`,它们分别执行加和减运算。一些其他可获得的操作是位操作 `fetch_or` 和 `fetch_and`,以及用于比较最大值和最小值的 `fetch_max` 和 `fetch_min`。 205 | 206 | 它们的函数签名如下,使用 AtomicI32 作为示例: 207 | 208 | ```rust 209 | impl AtomicI32 { 210 | pub fn fetch_add(&self, v: i32, ordering: Ordering) -> i32; 211 | pub fn fetch_sub(&self, v: i32, ordering: Ordering) -> i32; 212 | pub fn fetch_or(&self, v: i32, ordering: Ordering) -> i32; 213 | pub fn fetch_and(&self, v: i32, ordering: Ordering) -> i32; 214 | pub fn fetch_nand(&self, v: i32, ordering: Ordering) -> i32; 215 | pub fn fetch_xor(&self, v: i32, ordering: Ordering) -> i32; 216 | pub fn fetch_max(&self, v: i32, ordering: Ordering) -> i32; 217 | pub fn fetch_min(&self, v: i32, ordering: Ordering) -> i32; 218 | pub fn swap(&self, v: i32, ordering: Ordering) -> i32; // "fetch_store" 219 | } 220 | ``` 221 | 222 | 最后一个例外的操作是将一个新值存储到原子变量中,而不考虑原来的值。它不叫做 `fetch_store`,而是称为 `swap`。 223 | 224 | 这里有一个快速的演示,展示了 `fetch_add` 如何在操作之前返回值: 225 | 226 | ```rust 227 | use std::sync::atomic::AtomicI32; 228 | 229 | let a = AtomicI32::new(100); 230 | let b = a.fetch_add(23, Relaxed); 231 | let c = a.load(Relaxed); 232 | 233 | assert_eq!(b, 100); 234 | assert_eq!(c, 123); 235 | ``` 236 | 237 | fetch_add 操作从 100 递增到 123,但是返回给我们还是旧值 100。任意下一个操作将看见 123。 238 | 239 | 来自这些操作的返回值并不总是相关的。如果你仅需要将操作用于原子值,但是值本身并没有用,那么你可以忽略该返回值。 240 | 241 | 需要记住的一个重要的事情是,fetch_add 和 fetch_sub 为溢出实现了环绕(wrapping)行为。将值递增超过最大可表示值将导致环绕到最小可表示值。这与常规整数上的递增和递减行为是不同的,后者在调试模式下的溢出将 panic。 242 | 243 | 在[“「比较并交换」操作”](#比较并交换操作)中,我们将看见如何使用溢出检查进行原子加运算。 244 | 245 | 但首先,让我们看看这些方法的现实使用示例。 246 | 247 | ### 示例:来自多线程的进度报道 248 | 249 | (英文版本) 250 | 251 | 在[“示例:进度报道”](#示例进度报道)中,我们使用一个 AtomicUsize 去报道后台线程的进度。如果我们把工作分开,例如,四个线程,每个处理 25 个元素,我们将需要知道所有 4 个线程的进度。 252 | 253 | 我们可以为每个线程使用单独的 AtomicUsize 并且在主线程加载它们并进行汇总,但是更简单的解决方案是,使用单个 AtomicUsize 去跟踪所有线程处理元素的总数。 254 | 255 | 为了使其工作,我们不再使用 store 方法,因为这会覆盖其他线程的进度。相反,我们可以使用原子自增操作在每个处理元素之后递增计数器。 256 | 257 | ```rust 258 | fn main() { 259 | let num_done = &AtomicUsize::new(0); 260 | 261 | thread::scope(|s| { 262 | // 4 个后台线程,去处理所有 100 个元素,每个 25 次。 263 | for t in 0..4 { 264 | s.spawn(move || { 265 | for i in 0..25 { 266 | process_item(t * 25 + i); // 假设此处理需要花费一些时间。 267 | num_done.fetch_add(1, Relaxed); 268 | } 269 | }); 270 | } 271 | 272 | // 主线程每秒展示一次状态更新。 273 | loop { 274 | let n = num_done.load(Relaxed); 275 | if n == 100 { break; } 276 | println!("Working.. {n}/100 done"); 277 | thread::sleep(Duration::from_secs(1)); 278 | } 279 | }); 280 | 281 | println!("Done!"); 282 | } 283 | ``` 284 | 285 | 一些东西已经改变。更重要地是,我们现在产生了 4 个后台线程而不是 1 个,并且使用 fetch_add 而不是 store 去修改 `num_done` 原子变量。 286 | 287 | 更巧妙的是,我们现在对后台线程使用一个 move 闭包,并且 num_done 现在是一个引用。这与我们使用 fetch_add 无关,而是我们如何在循环中产生 4 个线程有关。此闭包捕获 t,以了解它是 4 个线程中的哪一个,从而确定是从元素 0、25、50 还是 75 开始。没有 move 关键字,闭包将尝试通过引用捕获 t。这是不允许的,因为它仅在循环期间短暂地存在。 288 | 289 | 由于 move 闭包,它移动(或复制)它的捕获而不是借用它们,这使它得到 t 的复制。因为它也捕获 num_done,我们已经改变该变量为一个引用,因为我们仍然想要借用相同的 AtomicUsize。注意,原子类型并没有实现 Copy trait,所以如果我们尝试移动一个原子类型的变量到多个线程,我们将得到错误。 290 | 291 | 撇开闭包的微妙不谈,在这里使用 fetch_add 更改是非常简单的。我们并不知道线程将以哪种顺序递增 num_done,但由于加运算是原子的,我们并不担心任何事情,并且当所有线程完成时,可以确信它将是 100。 292 | 293 | ### 示例:统计数据 294 | 295 | (英文版本) 296 | 297 | 继续通过原子报道其他线程正在做什么的概念,让我们拓展我们的示例,也可以收集和报道一些关于处理元素所花费时间的统计数据。 298 | 299 | 在 num_done 旁边,我们递增了两个原子变量 `total_time` 和 `max_time`,以便跟踪处理元素所花费的时间。我们将使用这些报道平均和峰值处理时间。 300 | 301 | ```rust 302 | fn main() { 303 | let num_done = &AtomicUsize::new(0); 304 | let total_time = &AtomicU64::new(0); 305 | let max_time = &AtomicU64::new(0); 306 | 307 | thread::scope(|s| { 308 | /// 4 个后台线程,去处理所有 100 个元素,每个 25 次。 309 | for t in 0..4 { 310 | s.spawn(move || { 311 | for i in 0..25 { 312 | let start = Instant::now(); 313 | process_item(t * 25 + i); // 假设此处理需要花费一些时间。 314 | let time_taken = start.elapsed().as_micros() as u64; 315 | num_done.fetch_add(1, Relaxed); 316 | total_time.fetch_add(time_taken, Relaxed); 317 | max_time.fetch_max(time_taken, Relaxed); 318 | } 319 | }); 320 | } 321 | 322 | // 主线程每秒展示一次状态更新。 323 | loop { 324 | let total_time = Duration::from_micros(total_time.load(Relaxed)); 325 | let max_time = Duration::from_micros(max_time.load(Relaxed)); 326 | let n = num_done.load(Relaxed); 327 | if n == 100 { break; } 328 | if n == 0 { 329 | println!("Working.. nothing done yet."); 330 | } else { 331 | println!( 332 | "Working.. {n}/100 done, {:?} average, {:?} peak", 333 | total_time / n as u32, 334 | max_time, 335 | ); 336 | } 337 | thread::sleep(Duration::from_secs(1)); 338 | } 339 | }); 340 | 341 | println!("Done!"); 342 | } 343 | ``` 344 | 345 | 后台线程现在使用 `Instant::now()` 和 `Instant::elapsed()` 去衡量它们在 `process_item()` 所花费的时间。原子的递增操作用于将微秒数递增到 total_time,并且原子的 max 操作用于跟踪 max_time 中的最高测量值。 346 | 347 | 主线程将总时间除以处理器元素的数量以获取平均处理时间,然后将它与 max_time 的峰值时间一起报道。 348 | 349 | 由于三个原子变量是单独更新的,因此主线程可能在线程递增 num_done 后而在更新 total_time 之前加载值,导致低估了平均值。更微妙的是,因为 Relaxed 内存排序不能保证从另一个线程中看到操作的相对顺序,它甚至可能看到 total_time 新更新的值,同时仍然看到 num_done 的旧值,导致平均值的高估。 350 | 351 | 在我们的示例中,这两个都不是大问题。最糟糕的情况是向用户提交了不准确的平均值。 352 | 353 | 如果我们想要避免这个,我们可以把这三个统计数据放到一个 Mutex 中。然后,在更新三个数字时,我们短暂地锁定 mutex,这三个数字本身就不再是原子的。这有效地转变三次更新为单个原子操作,代价是锁定和解锁 mutex 的开销,并且可能临时地阻塞线程。 354 | 355 | ### 示例:ID 分配 356 | 357 | (英文版本) 358 | 359 | 让我们转到一个用例,我们实际上需要 `fetch_add` 的返回值。 360 | 361 | 假设我们需要一些函数,`allocate_new_id()`,每次调用它时,都会给出新的唯一的数字。我们可能使用这些数字标识程序中的任务或其它事情;需要一个小而易于存储和在线程之间传递的东西来唯一标识事物,例如整数。 362 | 363 | 使用 `fetch_add` 实现此函数是轻松的: 364 | 365 | ```rust 366 | use std::sync::atomic::AtomicU32; 367 | 368 | fn allocate_new_id() -> u32 { 369 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 370 | NEXT_ID.fetch_add(1, Relaxed) 371 | } 372 | ``` 373 | 374 | 我们只是跟踪了*下一个*要给出的数字,并在每次加载时递增它。第一个调用者将得到 0,第二个调用者将得到 1,以此类推。 375 | 376 | 这里唯一的问题是溢出时包装行为。第 4,294,967,296 次调用将溢出 32 位整数,因此下一次调用将再次返回 0。 377 | 378 | 这是否是一个问题取决于用例:经常被这样调用的可能性是多大,如果数字不是唯一的,最糟糕的情况是什么?虽然这似乎是一个巨大的数字,现代计算机也可以在几秒内的轻松执行我们的函数。如果内存安全取决于这些数字的唯一性,那么我们上面的实现是不可接受的。 379 | 380 | 为了解决这个问题,如果调用次数太多,我们可以试图去使函数 panic,例如这样: 381 | 382 | ```rust 383 | // 这个版本是有问题的。 384 | fn allocate_new_id() -> u32 { 385 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 386 | let id = NEXT_ID.fetch_add(1, Relaxed); 387 | assert!(id < 1000, "too many IDs!"); 388 | id 389 | } 390 | ``` 391 | 392 | 现在,assert 语句将在 1000 次调用后 panic。然而,这在原子加运算已经发生后发生,这意味着当我们 panic 时,`NEXT_ID` 已经递增到 1001。如果另一个线程在之后调用该函数,它将在 panic 之前递增到 1002,以此类推。虽然需要很长时间,我们将在 4,294,966,296 次 panic 过后,NEXT_ID 将再次溢出到 0 时,遇到相同的问题。 393 | 394 | 有三个通常的方式解决这个问题。第一种是在溢出时不使用 panic,而是完全终止进程。`std::process::abort` 函数将终止整个函数,排除任何继续调用我们函数的可能性。尽管终止进程可能需要短暂的时间,但是函数仍然可能通过其它线程调用,但在程序真正的终止之前,发生数十亿次调用的可能性几乎可以忽略不计。 395 | 396 | 事实上,在标准库中的 `Arc::clone()` 溢出检查就是这么实现的,以防你在某种方式下克隆 `isize::MAX` 次。在 64 位计算机上,这需要上百年的时间,但如果 isize 只有 32 位,这仅需要几秒钟。 397 | 398 | 处理溢出的第二种方法是使用 fetch_sub 在 panic 之前再次递减计数器,就像这样: 399 | 400 | ```rust 401 | fn allocate_new_id() -> u32 { 402 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 403 | let id = NEXT_ID.fetch_add(1, Relaxed); 404 | if id >= 1000 { 405 | NEXT_ID.fetch_sub(1, Relaxed); 406 | panic!("too many IDs!"); 407 | } 408 | id 409 | } 410 | ``` 411 | 412 | 当多个线程在相同时间执行这个函数,计数器仍然有可能在非常短暂的时间超过 100,但这受到活动线程数量的限制。合理地假设是永远不会有数十亿个激活的线程,并非所有线程都在 fetch_add 和 fetch_sub 之间的短暂时间内同时执行相同的函数。 413 | 414 | 这就是标准库 `thread::scope` 实现中处理运行线程数量溢出的方式。 415 | 416 | 第三种处理溢出的方式可以说是唯一正确的方式,因为如果它溢出,它完全可以阻止加运算发生。然而,我们不能使用迄今为止看到的原子操作实现这一点。为此,我们需要「比较并交换」操作,接下来我们将探索。 417 | 418 | ## 比较并交换操作 419 | 420 | (英文版本) 421 | 422 | 更加高级和灵活的原子操作是「*比较并交换*」操作。这个操作检查是否原子值等同于给定的值,只有在这种情况下,它才以原子地方式使用新值替换它,作为单个操作完成。它会返回先前的值,并告诉我们是否进行了替换。 423 | 424 | 它的签名比我们到目前为止看到的要复杂一些。以 AtomicI32 为例,它看起来像这样: 425 | 426 | ```rust 427 | impl AtomicI32 { 428 | pub fn compare_exchange( 429 | &self, 430 | expected: i32, 431 | new: i32, 432 | success_order: Ordering, 433 | failure_order: Ordering 434 | ) -> Result; 435 | } 436 | ``` 437 | 438 | 暂时忽略内存排序,它基本上与以下实现相同,只是这一切都发生在单个不可分割原子操作上: 439 | 440 | ```rust 441 | impl AtomicI32 { 442 | pub fn compare_exchange(&self, expected: i32, new: i32) -> Result { 443 | // 在实际中,加载、比较以及存储操作, 444 | // 这些所有都是以单个原子操作发生的。 445 | let v = self.load(); 446 | if v == expected { 447 | // 值符合预期 448 | // 替换它并报道成功。 449 | self.store(new); 450 | Ok(v) 451 | } else { 452 | // 值不符合预期。 453 | // 保持不变并报道失败。 454 | Err(v) 455 | } 456 | } 457 | } 458 | ``` 459 | 460 | 使用该函数,我们可以从原子变量中加载值,执行我们喜欢的任何计算,并且然后仅原子变量在此期间没有改变值的情况下,再存储新的计算值。如果我们把这个放在一个循环中重试,如果它确实发生了变化,我们可以使用它来实现所有其它原子操作,使它成为最通用的一个。 461 | 462 | 为了演示,让我们在不使用 `fetch_add` 的情况下将 AtomicU32 递增 1,仅是为了看看 compare_exchange 是如何使用的: 463 | 464 | ```rust 465 | fn increment(a: &AtomicU32) { 466 | let mut current = a.load(Relaxed); // 1 467 | loop { 468 | let new = current + 1; // 2 469 | match a.compare_exchange(current, new, Relaxed, Relaxed) { // 3 470 | Ok(_) => return, // 4 471 | Err(v) => current = v, // 5 472 | } 473 | } 474 | } 475 | ``` 476 | 477 | 1. 首先,我们加载 a 的当前值。 478 | 2. 我们计算我们想要存储在 a 的新值,而不考虑其他线程的并发修改。 479 | 3. 我们使用 compare_exchange 去更新 a 的值,但*仅*当它的值仍然与我们之前加载的值相同时。 480 | 4. 如果 a 确实和之前一样,它现在被我们的新值所取代,我们就完成了。 481 | 5. 如果 a 并不和之前相同,那么自从我们加载它以来,它一定短时间被另一个线程改变了。`compare_exchange` 操作给我们提供了 a 的改变值,并且我们将再次尝试使用该值。加载和更新之间的时间非常短暂,它不可能循环超过几次迭代。 482 | 483 | > 如果原子变量从某个值 A 更改到 B,但在 load 操作之后和 compare_exchange 操作之前又变回 A,即使原子变量在此期间发生了变化(并且回变),compare_exchange 操作也会成功。在很多示例中,就像在我们的递增示例中一样,这并不是问题。然而,有几种算法,通常涉及原子指针,这样的情况就会产生问题。这就是所谓的 ABA 问题。 484 | 485 | 在 `compare_exchange` 旁边,有一个名为 `compare_exchange_weak` 的类似方法。区别是 weak 版本有时可能仍然保留不改变值并且返回 Err,即使原子值匹配期待值。在某些平台,这个方法可以更有效地实现,并且对于虚假的「比较并交换」失败的后果不重要的情况下,比如上面的递增函数,应该优先使用它。在[第七章节](./7_Understanding_the_Processor.md),我们将深入研究底层细节,以找出为什么 weak 版本会更有效。 486 | 487 | ### 示例:没有溢出的 ID 分配 488 | 489 | (英文版本) 490 | 491 | 现在,从[“示例:ID 分配”](#示例id-分配)中回到 `allocate_new_id()` 的溢出问题。 492 | 493 | 为了停止递增 NEXT_ID 超过某个限制以阻止溢出,我们可以使用 compare_exchange 去实现具有上限的原子操作加。使用这个想法,让我们制作一个始终正确处理溢出 allocate_new_id 的版本,即使在几乎不可能的情况下也是如此: 494 | 495 | ```rust 496 | fn allocate_new_id() -> u32 { 497 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 498 | let mut id = NEXT_ID.load(Relaxed); 499 | loop { 500 | assert!(id < 1000, "too many IDs!"); 501 | match NEXT_ID.compare_exchange_weak(id, id + 1, Relaxed, Relaxed) { 502 | Ok(_) => return id, 503 | Err(v) => id = v, 504 | } 505 | } 506 | } 507 | ``` 508 | 509 | 现在,我们在修改 NEXT_ID 之前,检查并 panic,保证它将从不递增超出 1000,使溢出变得不可能。如果我们愿意,我们现在可以将上限从 1000 提高到 `u32::MAX`,而不必担心它可能会超过极限的边缘情况。 510 | 511 |
512 |

Fetch-Update

513 |

原子类型有一个名为 fetch_update 的简写方法,用于「比较并交换」循环模式。它相当于 load 操作,然后就是重复计算和 compare_exchange_weak 的循环,就像我们上面做的那样。

514 | 515 |

使用它,我们可以使用一行实现我们的 allocate_new_id:

516 | 517 |
518 |   NEXT_ID.fetch_update(Relaxed, Relaxed,
519 |       |n| n.checked_add(1)).expect("too many IDs!")
520 | 521 |

有关详细信息,请查看该方法的文档。

522 | 523 |

我们不会在本书中使用 fetch_update 方法,因此我们可以专注于单个原子操作。

524 |
525 | 526 | ### 示例:惰性一次性初始化 527 | 528 | (英文版本) 529 | 530 | 在[“示例:惰性初始化”](#示例惰性初始化)中,我们查看常量值的惰性初始化示例。我们做了一个函数,在第一次调用时惰性地初始化一个值,但在以后的调用中重用它。当多个线程并发地运行这个函数,多个线程可能执行初始化,并且它们将以不可预期的顺序覆盖彼此的结果。 531 | 532 | 对于我们期望值是常量,或者当我们不关心改变值时,这很好。然而,也有些用例,这些值每次都会初始化为不同的值,即便我们需要在程序的一次运行中返回相同的值。 533 | 534 | 例如,想象一个函数 `get_key()`,它返回一个随机生成的密钥,该密钥仅在程序每次运行时生成。它可能是用于与程序通信的加密密钥,该密钥每次运行程序时都需要是唯一的,但在进程中保持不变。 535 | 536 | 这意味着我们不能在生成密钥之后,简单地使用一个 store 操作,因为这可能仅在片刻之后由其他线程复写这个生成的密钥,导致两个线程使用不同的密钥。相反,我们可以使用 compare_exchange 去确保我们仅当没有其他线程完成该操作,去存储这个密钥,否则,扔掉我们的密钥,改用存储的密钥。 537 | 538 | ```rust 539 | fn get_key() -> u64 { 540 | static KEY: AtomicU64 = AtomicU64::new(0); 541 | let key = KEY.load(Relaxed); 542 | if key == 0 { 543 | let new_key = generate_random_key(); // 1 544 | match KEY.compare_exchange(0, new_key, Relaxed, Relaxed) { // 2 545 | Ok(_) => new_key, // 3 546 | Err(k) => k, // 4 547 | } 548 | } else { 549 | key 550 | } 551 | } 552 | ``` 553 | 554 | 1. 我们仅在 KEY 仍然没有初始化时,生成一个新的密钥。 555 | 2. 我们用新生成的密钥替换 KEY,但前提是它仍然是 0。 556 | 3. 如果我们将 0 换成新密钥,我们将返回新生成的密钥。`get_key()` 的新调用将返回现在存储在 KEY 中的相同新密钥。 557 | 4. 如果我们输给了另一个初始化 KEY 的线程,我们忘记我们的新密钥,而是使用来自 KEY 的密钥。 558 | 559 | 这是一个很好的例子,在这里 `compare_exchange` 比 `weak` 变体更合适。我们不会在循环中运行「比较并交换」操作,如果操作虚假失败,我们不想返回 0。 560 | 561 | 正如[“示例:惰性初始化”](#示例惰性初始化)中提到的,如果 `generate_random_key()` 需要大量时间,那么在初始化期间阻塞线程可能更有意义,以避免可能花费时间生成不会使用的密钥。Rust 标准库通过 `std::sync::Once` 和 `std::sync::OnceLock` 提供此类功能。 562 | 563 | ## 总结 564 | 565 | (英文版本) 566 | 567 | * 原子操作是不可分割的;它们要么完整的完成,要么它们还没有发生。 568 | * 在 Rust 中的原子操作是通过 `std::sync::atomic` 原子类型完成的,例如 `AtomicI32`。 569 | * 不是所有原子类型在所有平台都是可获得的。 570 | * 当涉及多个变量时,原子操作的相对顺序是棘手的。更多细节,请看[第三章](./3_Memory_Ordering.md)。 571 | * 简单的 load 和 store 操作非常适合非常简单的基本线程间通信,例如停止标识和状态报道。 572 | * 我们可以使用竞争条件[^2]来惰性始化,而不会引发数据竞争[^3]。 573 | * 「获取并修改」操作允许进行一小组基本的原子修改操作,当多个线程同时修改同一个原子变量时,非常有用。 574 | * 原子加和减运算在溢出时会默默地进行环绕(wrap around)操作。 575 | * 「比较并交换」操作是最灵活和通用的,并且是任意其它原子操作的基石。 576 | * *weak* 版本「比较并交换」稍微更有效。 577 | 578 |

579 | 下一篇,第三章:内存排序 580 |

581 | 582 | [^1]: 583 | [^2]: 竞争条件是指多个线程并发访问和修改共享数据时,其最终结果依赖于线程执行的具体顺序。在某些情况下,我们可以利用竞争条件来实现延迟初始化。也就是说,多个线程可以同时尝试对共享资源进行初始化,但只有第一个成功的线程会完成初始化,而其他线程会放弃初始化操作。 584 | [^3]: 数据竞争是指多个线程同时访问共享数据,并且至少有一个线程进行写操作,而没有适当的同步机制来保证正确的访问顺序。 585 | [^4]: 586 | -------------------------------------------------------------------------------- /3_Memory_Ordering.md: -------------------------------------------------------------------------------- 1 | # 第三章:内存排序[^1] 2 | 3 | (英文版本) 4 | 5 | 在[第二章](./2_Atomics.md),我们简要地谈到了内存排序的概念。在该章节,我们将研究这个主题,并探索所有可用的内存排序选项,并且,更重要地是,我们将学习如何使用它们。 6 | 7 | ## 重排和优化 8 | 9 | (英文版本) 10 | 11 | 处理器和编译器执行各种优化,以便使你的程序运行地尽可能地快。例如,处理器可能会确定你程序中的两个连续指令不会相互影响,如果顺序执行更快就这样执行,否则按不正常顺序执行。当一个指令在从主存中获取一些数据被短暂地阻塞了,只要这不会更改你程序的行为,几个后续地指令可能在第一个指令结束之前被执行和完成。类似地,编译器可能会决定重排或者重写你程序的部分代码,如果它有理由相信这可能会导致更快地执行。但是,同样,仅有在不更改你程序行为的情况下。 12 | 13 | 让我们来看看以下这个例子: 14 | 15 | ```rust 16 | fn f(a: &mut i32, b: &mut i32) { 17 | *a += 1; 18 | *b += 1; 19 | *a += 1; 20 | } 21 | ``` 22 | 23 | 这里,编译器肯定会明白,操作的顺序并不重要,因为在这三个加运算之间没有发生任何依赖于 `*a` 或 `*b` 的操作(假设溢出检查被禁用)。因此,编译器可能会重新排序第二个和第三个操作,然后将前两个操作合并为单个加运算: 24 | 25 | ```rust 26 | fn f(a: &mut i32, b: &mut i32) { 27 | *a += 2; 28 | *b += 1; 29 | } 30 | ``` 31 | 32 | 稍后,在执行优化编译程序的函数时,由于各种原因,处理器可能最终在执行第一次加运算之前,执行第二次加运算,可能是 `*b` 在缓存中可用,而 `*a` 在主内存可获取。 33 | 34 | 无论这些优化如何,结果都是相同的:`*a` 加上 2,`*b` 加上 1。它们执行加运算的顺序对于你程序的其余部分完全不可见。 35 | 36 | 验证特定的重新排序或者其他优化并不影响程序的行为的逻辑并不需要考虑其他线程。在我们上面的示例中,这是极好的,因为独占引用(`&mut i32`)保证没有其他线程可以访问这个值。出现问题的唯一情况是,当共享的数据在线程之间发生改变。或者,换句话说,当使用原子操作时。这就是为什么,我们必须明确地告诉编译器和处理器,它们可以和不能使用我们的原子操作做什么,因为它们通常的逻辑忽略了线程之间的交互,并且可能允许的优化,会导致我们程序的结果改变。 37 | 38 | 有趣的问题是我们*如何*告诉它们。如果我们想要准确地阐明什么是可以接受的,什么是不可以接受的,并发程序将变得非常冗长并很容易出错,并且可能特定于架构: 39 | 40 | ```rust 41 | let x = a.fetch_add(1, 42 | Dear compiler and processor, 43 | Feel free to reorder this with operations on b, 44 | but if there's another thread concurrently executing f, 45 | please don't reorder this with operations on c! 46 | Also, processor, don't forget to flush your store buffer! 47 | If b is zero, though, it doesn't matter. 48 | In that case, feel free to do whatever is fastest. 49 | Thanks~ <3 50 | ); 51 | ``` 52 | 53 | 的确,我们仅能从一小部分选项中进行选择,这些选项由 `std::sync::atomic::Ordering` 枚举表示,每个原子操作都将其作为参数。可用选项的部分是非常有限的,但是经过精心挑选,可以适用大部分用例。排序是非常抽象的,并且不能直接反映实际编译器和处理器涉及的机制,例如指令重排。这使得你的并发代码可以脱离架构并且面向未来。它允许在不知道每个当前和未来处理器版本的信息的情况下进行验证。 54 | 55 | 在 Rust 中可用的排序: 56 | 57 | * Relaxed 排序:`Ordering::Relaxed` 58 | * Release 和 acquire 排序:`Ordering::{Release, Acquire, AcqRel}` 59 | * 顺序一致性排序:`Ordering::SeqCst` 60 | 61 | 在 C++ 中,有一种叫做 *consume 排序*,它在 Rust 中被省略了,尽管如此,对它的讨论也是很有用的。 62 | 63 | ## 内存模型 64 | 65 | (英文版本) 66 | 67 | 不同的内存排序选项有一个严格的形式定义,以确保我们确切地知道我们允许假设什么,并且让编译器编写者确切知道它们需要向我们提供什么。为了将它与特定处理器架构的细节解耦,内存排序是根据抽象*内存模型*定义的。 68 | 69 | Rust 的内存模型,它更多的抄自 C++,与任何现有的处理器架构不匹配,而是一个抽象模型,它有一套严格的规则,试图代表当前和未来所有架构的最大公约数,同时给予编译器足够的自由去进行程序分析和优化时作出有用的假设。 70 | 71 | 我们已经在[第一章的“借用和数据竞争”](./1_Basic_of_Rust_Concurrency.md#借用和数据竞争)看到内存模型的一部分,我们讨论了数据竞争如何导致未定义行为。Rust 的内存模型允许并发的原子存储,但将并发的非原子存储到相同的变量视为数据竞争,这将导致未定义行为。 72 | 73 | 然而,在大多数处理器架构中,原子存储之间和常规非原子存储之间并没有什么区别,我们将在[第七章](./7_Understanding_the_Processor.md)看到这些。人们可以争辩说,内存模型的限制性比必要性要强,但这些严格的规则使编译器和程序员更容易对程序进行推理,并为未来的发展留下了空间。 74 | 75 | ## Happens-Before 关系 76 | 77 | (英文版本) 78 | 79 | 内存模型定义了操作在 *happens-before 关系*发生的顺序。这意味着,作为一个抽象模型,它不涉及机器指令、缓存、缓冲区、时间、指令重排、编译优化等,而只指定一件事情在另一件事情之前保证发生的情况,并将其它一切的顺序都视为未定义的。 80 | 81 | 基础的 happens-before 规则是同一线程内的任何事情都按顺序发生。如果线程线程正在执行 `f(); g();`,那么 `f()` 在 `g()` 之前发生。 82 | 83 | 然而,在线程之间,happens-before 仅发生在一些特定的情况下,例如,在创建和等待线程时,解锁和锁定 mutex,以及使用非 relaxed 的原子操作。Relaxed 内存排序是最基本的(也是性能最好的)内存排序,它本身并不会导致任何跨线程的 happens-before 关系。 84 | 85 | 为了探索这意味着什么,让我们看看以下示例,我们假设 a 和 b 有不同的线程并发执行: 86 | 87 | ```rust 88 | static X: AtomicI32 = AtomicI32::new(0); 89 | static Y: AtomicI32 = AtomicI32::new(0); 90 | 91 | fn a() { 92 | X.store(10, Relaxed); // 1 93 | Y.store(20, Relaxed); // 2 94 | } 95 | 96 | fn b() { 97 | let y = Y.load(Relaxed); // 3 98 | let x = X.load(Relaxed); // 4 99 | println!("{x} {y}"); 100 | } 101 | ``` 102 | 103 | 正如以上提及的,基础的 happens-before 规则是同一线程内的任何事情都按顺序发生。因在这个示例中,1 发生在 2 之前,并且 3 发生在 4 之前,正如 3-1 图片所示。因为我们使用 relaxed 内存排序,在我们的示例中并没有其它的 happens-before 关系。 104 | 105 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0301.png) 106 | 图3-1。示例代码中原子操作之间的 happens-before 关系。 107 | 108 | 如果 a 或 b 的任何一个在另一个开始之前完成,输出将是 0 0 或 10 20。如果 a 和 b 并发地运行,很容易看见输出是 10 0。发生这种操作的方式是,可能以以下顺序写运行:3 1 2 4。 109 | 110 | 更有趣地是,输出可以也是 0 20,尽管导致这个结果的操作不可能有全局地一致性顺序。当 3 被执行,它与 2 之间不存在 happens-before 关系,这意味着它可以加载 0 或 20。当 4 被执行,它与 1 之间不存在 happens-before 关系,这意味它可以加载 0 或 10。因此,输出 0 20 是一种有效的结果。 111 | 112 | 需要理解的重要和反直觉的是操作 3 加载值 20 并不能与 2 操作形成 happens-before 关系,即使这个值是由操作 2 存储的。我们对“之前”的概念的直觉是,在事情不一定按照全局一致性的顺序发生时会被打破,比如涉及指令重排的情况。 113 | 114 | 一个更有用并且直观,但是不太正式的理解是,从执行 b 的线程的视角来看,操作 1 和 2 可能以相反的顺序发生。 115 | 116 | ### spawn 和 join 117 | 118 | (英文版本) 119 | 120 | 产生的线程会创建一个 happens-before 关系,它将发生在 `spawn()` 之前的事件与新线程关联起来。同样地,join 线程创建一个 happens-before 关系,它将发生在 `join()` 调用之后的事件与被 join 的线程关联起来。 121 | 122 | 为了证明,以下示例中的断言不能失败: 123 | 124 | ```rust 125 | static X: AtomicI32 = AtomicI32::new(0); 126 | 127 | fn main() { 128 | X.store(1, Relaxed); 129 | let t = thread::spawn(f); 130 | X.store(2, Relaxed); 131 | t.join().unwrap(); 132 | X.store(3, Relaxed); 133 | } 134 | 135 | fn f() { 136 | let x = X.load(Relaxed); 137 | assert!(x == 1 || x == 2); 138 | } 139 | ``` 140 | 141 | 由于 join 和 spawn 操作形成 happens-before 关系,我们肯定知道 X 的 load 操作在第一个 store 之后,但在最后一个 store 之前,正如在图 3-2 所见。然而,它是否在第二个 store 操作之前或之后观察值是不可预测的。换句话说,它可能是 1 或 2,但不是 0 或 3。 142 | 143 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0302.png) 144 | 图 3-2。示例代码中 spawn、join、store 和 load 操作之间的 happens-before 关系。 145 | 146 | ## Relaxed 排序 147 | 148 | (英文版本) 149 | 150 | 当原子操作使用 relaxed 内存排序并不会提供任何 happens-before 关系,但是它们仍然保证了每个原子变量的*总的修改顺序*。这意味着,从线程的角度来看,*同一原子变量*的所有修改都是以相同的顺序进行的。 151 | 152 | 为了证明这意味着什么,我们假设 a 和 b 由不同的线程并发执行,考虑以下示例: 153 | 154 | ```rust 155 | static X: AtomicI32 = AtomicI32::new(0); 156 | 157 | fn a() { 158 | X.fetch_add(5, Relaxed); 159 | X.fetch_add(10, Relaxed); 160 | } 161 | 162 | fn b() { 163 | let a = X.load(Relaxed); 164 | let b = X.load(Relaxed); 165 | let c = X.load(Relaxed); 166 | let d = X.load(Relaxed); 167 | println!("{a} {b} {c} {d}"); 168 | } 169 | ``` 170 | 171 | 在该示例中,仅有一个线程修改 X,这使得很轻松地能够看到 X 的修改顺序:0→5→15。它从 0 开始,然后变成 5,最终变成 15。线程并不能从 X 中观察到与此总修改不一致的任何值。这意味着“0 0 0 0”、“0 0 5 15”和“0 15 15 15”是来自另一个线程打印语句的可能结果,而“0 5 0 15”或“0 0 10 15”的输出是不可能的。 172 | 173 | 即使原子变量有多个可能的修改顺序,所有线程也仅约定好一个顺序。 174 | 175 | 让我们用两个单独的函数替换 a1 和 a2,我们假设它们分别由一个单独的线程执行: 176 | 177 | ```rust 178 | fn a1() { 179 | X.fetch_add(5, Relaxed); 180 | } 181 | 182 | fn a2() { 183 | X.fetch_add(10, Relaxed); 184 | } 185 | ``` 186 | 187 | 假设这些是唯一修改 X 的线程,现在有两种修改顺序:要么是 0→5→15 或 0→10→15,这取决于哪个 fetch_add 操作先执行。无论哪种情况,所有线程都遵守相同的顺序。因此,即使我们即使我们有数百个额外的线程正在运行我们的 `b()` 函数,我们知道如果其中一个打印出 10,那么顺序必须是 0→10→15,而它们其中的任何一个都不可能打印出 5。反之亦然。 188 | 189 | 在[第二章](./2_Atomics.md),我们看见几个用例示例,其中保证个别变量的总修改顺序就足够了,使用 Relaxed 内存排序足够了。然而,如果我们尝试任何超出这些示例更高级的东西,我们将很快发现,需要比 relaxed 更强的保证。 190 | 191 |
192 |

凭空出现的值

193 |

在使用 Relaxed 内存排序时,由于缺乏顺序保证,当操作在循环方式下相互依赖时,可能会导致理论上的复杂情况。

194 | 195 |

为了演示,这里有一个人为的例子,两个线程从一个原子加载一个值,并将其存储在另一个原子中:

196 | 197 |
static X: AtomicI32 = AtomicI32::new(0);
198 | static Y: AtomicI32 = AtomicI32::new(0);
199 | 
200 | fn main() {
201 |     let a = thread::spawn(|| {
202 |         let x = X.load(Relaxed);
203 |         Y.store(x, Relaxed);
204 |     });
205 |     let b = thread::spawn(|| {
206 |         let y = Y.load(Relaxed);
207 |         X.store(y, Relaxed);
208 |     });
209 |     a.join().unwrap();
210 |     b.join().unwrap();
211 |     assert_eq!(X.load(Relaxed), 0); // 可能失败?
212 |     assert_eq!(Y.load(Relaxed), 0); // 可能失败?
213 | }
214 | 215 |

似乎很容易得出 X 和 Y 的值不会是除 0 以外的任何东西的结论,因为 store 操作仅从这项相同的原子中加载值,而这些原子仅是 0。

216 | 217 |

然而,如果我们严格遵循理论内存模型,我们必须面对循环推理,并得出可怕的结论,我们可能错了。事实上,内存模型在技术上允许出现这样的结果,即最终 X 和 Y 都是 37,或者任意其它的值,导致断言失败。

218 | 219 |

由于缺乏顺序保证,这两个线程的 load 操作可能都看到另一个线程 store 操作的结果,允许按操作顺序循环:我们在 Y 中存储 37,因为我们从 X 加载了 37,X 存储到 X,因为我们从 Y 加载了 37,这是我们在 Y 中存储的值。

220 | 221 |

幸运的是,这种凭空捏造值的可能性在理论模型中被普遍认为是一个 bug,而不需要你在实践中考虑。如何在不允许这种异常情况的情况下形式化 relaxed 内存排序还是一个未解决的问题。尽管这对于形式化验证来说可能是一个问题,让许多理论家夜不能寐,但是我们其他人可以放心地使用 relaxed,因为在实践中不会发生这种情况。

222 |
223 | 224 | ## Release 和 Acquire 排序 225 | 226 | (英文版本) 227 | 228 | *Release* 和 *Acquire* 内存排序通常成对使用,它们用于形成线程之间的 happens-before 关系。`Release` 内存排序适用于 store 操作,而 `Acquire` 内存排序适用于 load 操作。 229 | 230 | 当 acquire-load 操作观察 release-store 操作的结果时,就会形成 happens-before 关系。在这种情况下,store 操作及其之前的所有操作在时间上先于 load 操作和之后的所有操作。 231 | 232 | 当使用 Acquire 进行「获取并修改」或者「比较并交换」操作时,它仅适用于操作的 load 部分。类似地,Release 仅适用于操作的 store 部分。`AcqRel` 用于表示 Acquire 和 Release 的组合,这既能使 load 使用 Acquire,也能使 store 使用 Release。 233 | 234 | 让我们回顾一个示例,看看我们在实践中如何使用它们。在以下示例中,我们将一个 64 位整数从产生的线程发送到主线程。我们使用一个额外的原子布尔类型以指示主线程,整数已经被存储并且已经可以读取: 235 | 236 | ```rust 237 | use std::sync::atomic::Ordering::{Acquire, Release}; 238 | 239 | static DATA: AtomicU64 = AtomicU64::new(0); 240 | static READY: AtomicBool = AtomicBool::new(false); 241 | 242 | fn main() { 243 | thread::spawn(|| { 244 | DATA.store(123, Relaxed); 245 | READY.store(true, Release); // 在这个 store 之前的所有操作都会在这个 store 操作之后可见 246 | }); 247 | while !READY.load(Acquire) { // READY 的值变为 true,表示前面的 load 操作对于其他线程可见 248 | thread::sleep(Duration::from_millis(100)); 249 | println!("waiting..."); 250 | } 251 | println!("{}", DATA.load(Relaxed)); 252 | } 253 | ``` 254 | 255 | 当产生的线程完成数据存储时,它使用 release-store 去设置 `READY` 标识为真。当主线程通过它的 acquire-load 操作观察到时,会在这两个线程之间建立一个 happens-before 关系,正如图 3-3 所示。此时,我们肯定知道在 release-store 到 READY 之前的所有操作对 acquire-load 之后的所有操作都可见。具体而言,当主线程从 `DATA` 加载时,我们可以肯定它将加载由后台线程存储的值。该程序在最后一行只有一种输出结果:123。 256 | 257 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0303.png) 258 | 259 | 图 3-3。示例代码中原子操作之间的 happens-before 关系,展示了通过 acquire 和 release 操作形成的跨线程关系。 260 | 261 | 如果我们在这个示例为所有操作使用 relaxed 内存排序,主线程可能会看到 `READY` 翻转为 true,而之后仍然从 DATA 中加载 0。 262 | 263 | > “Release”和“Acquire”的名称基于它们最基本用例:一个线程通过原子地存储一些值到原子变量来发布数据,而另一个线程通过原子地加载这个值来获取数据。这正是当我们解锁(释放)互斥体并随后在另一个线程上锁定(获取)它时发生的情况。 264 | 265 | 在我们的示例中,来自 READY 标识的 happens-before 关系保证了 DATA 的 store 和 load 操作不能并发地发生。这意味着我们实际上不需要这些操作是原子的。 266 | 267 | 然而,如果我们仅是为我们的数据变量尝试去使用常规的非原子类型,编译器将拒绝我们的程序,因为当另一个线程也在借用它们,Rust 的类型系统不允许我们修改它们。类型系统不会理解我们在这里创建的 happens-before 关系。一些不安全的代码是必要的,以向编译器承诺我们已经仔细考虑过这个问题,我们确信我们没有违反任何规则,如下所示: 268 | 269 | ```rust 270 | static mut DATA: u64 = 0; 271 | static READY: AtomicBool = AtomicBool::new(false); 272 | 273 | fn main() { 274 | thread::spawn(|| { 275 | // 安全性:没有其他东西正在访问 DATA, 276 | // 因为我们还没有设置 READY 标识。 277 | unsafe { DATA = 123 }; 278 | READY.store(true, Release); // 在这个 store 之前的所有操作都会在这个 store 操作之后可见 279 | }); 280 | while !READY.load(Acquire) { // READY 的值变为 true,表示前面的 store 操作对于其他线程可见 281 | thread::sleep(Duration::from_millis(100)); 282 | println!("waiting..."); 283 | } 284 | // 安全地:没有其他东西修改 DATA,因为 READY 已设置。 285 | println!("{}", unsafe { DATA }); 286 | } 287 | ``` 288 | 289 |
290 |

更正式地

291 |

当 acquire-load 操作观察 release-store 操作的结果时,就会形成 happens-before 关系。但那是什么意思?

292 | 293 |

想象一下,两个线程都将一个 7 release-store 到相同的原子变量中,第三个线程从该变量中加载 7。第三个线程和第一个或者第二个线程有一个 happens-before 关系吗?这取决于它加载“哪个 7”:线程一还是线程二的。(或许一个不相关的 7)。这使我们得出的结论是,尽管 7 等于 7,但两个 7 与两个线程有一些不同。

294 | 295 |

思考这个问题的方式是我们在“Relaxed 排序”中讨论的总修改顺序:发生在原子变量上的所有修改的有序列表。即使将相同的值多次写入相同的变量,这些操作中的每一个都以该变量的总修改顺序代表一个单独的事件。当我们加载一个值,加载的值与每个变量“时间线”上的特定点相匹配,这告诉我们我们可能会同步哪个操作。

296 | 297 |

例如,如果原子总修改顺序是

298 | 299 |

1. 初始化为 0

300 | 301 |

2. Release-store 7(来自线程二)

302 | 303 |

3. Release-store 6

304 | 305 |

4. Release-store 7(来自线程一)

306 | 307 |

然后,acquire-load 7 将与第二个线程的 release-store 或者最后一个事件的 release-store 同步。然而,如果我们之前(就 happens-before 关系而言)见过 6,我们知道我们看到的是最后一个 7,而不是第一个 7,这意味着我们现在与线程一有 happens-before 的关系,而不是线程二。

308 | 309 |

还有一个额外的细节,即 release-stored 的值可以经过被任意数量的「获取并修改」和「比较并交换」操作修改,但仍会导致与读取最终结果的 acquire-load 操作建立 happens-before 关系。

310 | 311 |

例如,想象一个具有以下总修改顺序的原子变量:

312 |

1. 初始化为 0

313 | 314 |

2. Release-store 7

315 | 316 |

3. Relaxed-fetch-and-add 1,改变 7 到 8

317 | 318 |

4. Relaxed-fetch-and-add 1,改变 8 到 9

319 | 320 |

5. Release-store 7

321 | 322 |

6. Relaxed-swap 10,改变 7 到 10

323 | 324 |

现在,如果我们在这个变量上执行 acquire-load 到 9,我们不仅与第四个操作(存储此值)建立了一个 happens-before 关系,同时也与第二个操作(存储 7)建立了该关系,即使第三个操作使用了 Relaxed 内存排序。

325 | 326 |

相似地,如果我们在这个变量上执行 acquire-load 到 10,而该值是由一个 relaxed 操作写入的,我们仍然建立了与第五个操作(存储 7)的 happens-before 关系。因为它只是一个普通的 store 操作(不是「获取并修改」或「比较并交换」操作),它打破了规则链:我们没有与其他操作建立 happens-before 关系。

327 |
328 | 329 | ### 示例:锁定 330 | 331 | (英文版本) 332 | 333 | 互斥锁是 release 和 acquire 排序的最常见用例(参见[第一章的“锁:互斥锁和读写锁”](./1_Basic_of_Rust_Concurrency.md#锁互斥锁和读写锁))。当锁定时,它们使用 acquire 排序的原子操作来检查是否它已解锁,同时也(原子地)改变状态到“锁定”。当解锁时,它们使用 release 排序设置状态到“解锁”。这意味着,在解锁 mutex 和随后锁定它有一个 happens-before 关系。 334 | 335 | 以下是这种模式的演示: 336 | 337 | ```rust 338 | static mut DATA: String = String::new(); 339 | static LOCKED: AtomicBool = AtomicBool::new(false); 340 | 341 | fn f() { 342 | if LOCKED.compare_exchange(false, true, Acquire, Relaxed).is_ok() { 343 | // 安全地:我们持有独占的锁,所以没有其他东西访问 DATA。 344 | unsafe { DATA.push('!') }; 345 | LOCKED.store(false, Release); 346 | } 347 | } 348 | 349 | fn main() { 350 | thread::scope(|s| { 351 | for _ in 0..100 { 352 | s.spawn(f); 353 | } 354 | }); 355 | } 356 | ``` 357 | 358 | 正如我们在[第二章“「比较并交换」操作”](./2_Atomics.md#比较并交换操作)简要地所见,「比较并交换」接收两个内存排序参数:一个用于比较成功且 store 发生的情况,一个用于比较失败且 `store` 没有发生的情况。在 f 中,我们试图去改变 `LOCKED` 的值从 false 到 true,并且只有在成功的情况下才能访问 DATA。所以,我们仅关心成功的内存排序。如果 `compare_exchange` 操作失败,那一定是因为 `LOCKED` 已经设置为 true,在这种情况下 f 不会做任何事情。这与常规 mutex 上的 `try_lock` 操作相匹配。 359 | 360 | > 观察力强的读者可能已经注意到,「比较并交换」操作也可能是 swap 操作,因为在已锁定时将 true 替换为 true 不会改变代码的正确性: 361 | > 362 | > ```rust 363 | > // 这也有效。 364 | > if LOCKED.swap(true, Acquire) == false { 365 | > // … 366 | > } 367 | > ``` 368 | 369 | 归功于 acquire 和 release 内存排序,我们肯定没有两个线程能并发地访问数据。正如在图 3-4 展示的,对 DATA 的任何先前访问都在随后使用 release-store 操作将 false 存储到 LOCKED 之前发生,然后在下一个 acquire-compare-exchange(或 acquire-swap)操作中将 false 更改为 true,然后在下一次访问 DATA 之前发生。 370 | 371 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0304.png) 372 | 图 3-4。锁定示例中原子操作之间的 happens-before 关系,显示了两个线程按顺序锁定和解锁。 373 | 374 | 在[第四章](./4_Building_Our_Own_Spin_Lock.md),我们将把这个概念变成一个可重复使用的类型:自旋锁。 375 | 376 | ### 示例:使用间接的方式惰性初始化 377 | 378 | (英文版本) 379 | 380 | 在[第二章的“示例:惰性一次性初始化”](./2_Atomics.md#示例惰性一次性初始化)中,我们实现一个全局变量的惰性初始化,使用「比较并交换」操作去处理多个线程竞争同时初始化值的情况。由于该值是非零的 64 位整数,我们能够使用 AtomicU64 来存储它,在初始化之前使用零作为占位符。 381 | 382 | 要对不适合单个原子变量的更大的数据类型做同样的事情,我们需要寻找替代方案。 383 | 384 | 在这个例子中,假设我们想保持非阻塞行为,这样线程就不会等待另一个线程,而是从第一个线程中竞争并获取值来完成初始化。这意味着我们仍然需要能够在单个原子操作中从“未初始化”到“完全初始化”。 385 | 386 | 正如软件工程的基本定理告诉我们的那样,计算机科学中的每个问题都可以通过添加另一层间接来解决,这个问题也不例外。由于我们无法将数据放入单个原子变量中,因此我们可以使用原子变量来存储指向数据的*指针*。 387 | 388 | `AtomicPtr` 是 `*mut T` 的原子版本:指向 T 的指针。我们可以使用空指针作为初始状态的占位符,并使用「比较并交换」操作将其原子地替换为指向新分配的、完全初始化的 T 的指针,然后可以由其他线程读取。 389 | 390 | 由于我们不仅共享包含指针的原子变量,还共享它所指向的数据,因此我们不能再像[第 2 章](./2_Atomics.md)那样使用 Relaxed 的内存排序。我们需要确保数据的分配和初始化不会与读取数据竞争。换句话说,我们需要在 store 和 load 操作上使用 release 和 acquire 排序,以确保编译器和处理器不会通过(例如)重新排序指针的存储和数据本身的初始化来破坏我们的代码。 391 | 392 | 对于一些名为 Data 的任意数据类型,这引出了以下实现: 393 | 394 | ```rust 395 | use std::sync::atomic::AtomicPtr; 396 | 397 | fn get_data() -> &'static Data { 398 | static PTR: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); 399 | 400 | let mut p = PTR.load(Acquire); 401 | 402 | if p.is_null() { 403 | p = Box::into_raw(Box::new(generate_data())); 404 | if let Err(e) = PTR.compare_exchange( 405 | std::ptr::null_mut(), p, Release, Acquire 406 | ) { 407 | // 安全性:p 来自上方的 Box::into_raw, 408 | // 并且不能与其他线程共享 409 | drop(unsafe { Box::from_raw(p) }); 410 | p = e; 411 | } 412 | } 413 | 414 | // 安全性:p 不是 null,并且指向一个正确初始化的值。 415 | unsafe { &*p } 416 | } 417 | ``` 418 | 419 | 如果我们以 acquire-load 操作从 PTR 得到的指针是非空的,我们假设它指向已初始化的数据,并构建对该数据的引用。 420 | 421 | 然而,如果它仍然为空,我们会生成新数据,并使用 `Box::new` 将其存储在新的内存分配中。然后,我们使用 `Box::into_raw` 将此 `Box` 转换为原始指针,因此我们可以尝试使用「比较并交换」操作将其存储到 PTR 中。如果另一个线程赢得初始化竞争,`compare_exchange` 将失败,因为 PTR 不再是空的。如果发生这种情况,我们将原始指针转回 Box,使用 `drop` 来释放内存分配,避免内存泄漏,并继续使用另一个线程存储在 PTR 中的指针。 422 | 423 | 在最后的不安全块中,关于安全性的注释表明我们的假设是指它指向的数据已经被初始化。注意,这包括对事情发生顺序的假设。为了确保我们的假设成立,我们使用 release 和 acquire 内存排序来确保初始化数据实际上在创建对它引用之前已经发生。 424 | 425 | 我们在两个地方加载一个可能的非空(即初始化)指针:通过 load 操作和当 compare_exchange 失败时的该操作。因此,如上所述,我们需要在 load 内存排序和 compare_exchange 失败内存排序上都使用 Acquire,以便能够与存储指针的操作进行同步。当 compare_exchange 操作成功时,会发生 store 操作,因此我们必须使用 Release 作为其成功的内存排序。 426 | 427 | 图 3-5 显示了三个线程调用 `get_data()` 的情况的操作和发生前关系的可视化。在这种情况下,线程 A 和 B 都观察到一个空指针并都试图去初始化原子指针。线程 A 赢得竞争,导致线程 B 的 compare_exchange 调用失败。线程 C 在通过线程 A 初始化之后观察原子指针。最终结果是,所有三个线程最终都使用由线程 A 分配的 box。 428 | 429 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0305.png) 430 | 图3-5。调用 `get_data()` 的三个线程之间的操作和发生前关系。 431 | 432 | ## Consume 排序 433 | 434 | (英文版本) 435 | 436 | 让我们仔细看看上一个示例中的内存排序。如果我们把严格的模型放在一边,从更实际的方面来思考它,我们可以说 release 排序阻止了数据的初始化与共享指针的 store 操作重新排序。这一点非常重要,因为否则其它线程可能会在数据完全初始化之前就能看到它。 437 | 438 | 类似地,我们可以说 acquire 排序为防止重新排序,使得数据在加载指针之前被访问。然而,人们可能合理地质疑,在实践中是否有意义。在数据的地址被加载之前,如何访问数据?我们可能会得出结论:比 acquire 排序更弱的内存排序可能就足够了。我们的结论是正确的:这种较弱的内存排序被称为 consume 排序。 439 | 440 | consume 排序是 acquire 排序的一个轻量级、更高效的变体,其同步效果仅限于那些依赖于被加载值的操作。 441 | 442 | 这意味着如果你用 consume-load 从一个原子变量中加载一个通过 release 存储的值 x,那么基本上,这个 store 操作发生在依赖 x 的表达式(如 `*x`、`array[x]` 或 `table.lookup(x + 1)`)的求值之前,但不一定发生在不相关的操作(如读取另一个与 x 无关的变量)之前。 443 | 444 | 现在有好消息和坏消息。 445 | 446 | 好消息是,在所有现代处理器架构上,consume 排序是通过与 relaxed 排序完全相同的指令实现的。换句话说,consume 排序可以是“免费的”,而 acquire 内存排序在某些平台可能不是这样。 447 | 448 | 坏消息是,没有编译器真正实现 consume 排序。 449 | 450 | 事实证明,这种“依赖性”评估的概念不仅难以定义,而且在转换和优化程序时保持这些依赖性也很难。例如,编译器能够优化 x + 2 - x 为 2,有效地消除了对 x 的依赖。对于更复杂的表达式,如 `array[x]`,如果编译器能够对 x 或数组元素的可能值进行逻辑推断,那么可能会出现更微妙的变化。当考虑控制流,如 if 语句或函数调用时,问题将变得更加复杂。 451 | 452 | 因此,为了安全起见,编译器将 consume 排序升级到 acquire 排序。C++20 标准甚至明确地不鼓励使用 consume 排序,并指出,除了 acquire 排序之外,其他实现被证明是不可行的。 453 | 454 | 将来可能找到一个 consume 排序的有效定义和实现。然而,直到这一天到来之前,Rust 都不会暴露 `Ordering::Consume`。 455 | 456 | ## 顺序一致性排序 457 | 458 | (英文版本) 459 | 460 | 最强的内存排序是*顺序一致性*排序:`Ordering::SeqCst`。它包含了 acquire 排序(对于 load 操作)以及 release 排序(对于 store 操作)的所有保证,并且*也*保证了操作的全局一致排序。 461 | 462 | 这意味着在程序中使用 `SeqCst` 排序的每个操作是所有线程都同意的单独全序[^3](single total order,译注:可以理解为在该顺序关系中,每个操作都与其它操作有单独的顺序关系)的一部分。该全序[^4](total order,译注:该原子变量的顺序关系)与每个单独变量的总修改顺序(total modification order)一致。 463 | 464 | 由于它严格强于 acquire 和 release 内存排序,因此顺序一致性 load 或者 store 操作可以取代一对 release-acquire 中的 acquire-load 或 release-store 操作,形成 happens-before 关系。换句话说,acquire-load 不仅可以与 release-store 形成 happens-before 关系,同时也可以和顺序一致的 store 形成 happens-before 关系,同样 release-store 也是如此。 465 | 466 | > 仅有当 happens-before 关系的双方都使用 SeqCst 时,才能保证与 SeqCst 操作的单独全序一致。 467 | 468 | 虽然这似乎是最容易推理的内存排序,但 SeqCst 排序在实践中几乎从来都没有必要。在几乎所有情况下,通常 acquire 和 release 排序就足够了。 469 | 470 | 以下是一个依赖于顺序一致的有序操作的示例: 471 | 472 | ```rust 473 | use std::sync::atomic::Ordering::SeqCst; 474 | 475 | static A: AtomicBool = AtomicBool::new(false); 476 | static B: AtomicBool = AtomicBool::new(false); 477 | 478 | static mut S: String = String::new(); 479 | 480 | fn main() { 481 | let a = thread::spawn(|| { 482 | A.store(true, SeqCst); 483 | if !B.load(SeqCst) { 484 | unsafe { S.push('!') }; 485 | } 486 | }); 487 | 488 | let b = thread::spawn(|| { 489 | B.store(true, SeqCst); 490 | if !A.load(SeqCst) { 491 | unsafe { S.push('!') }; 492 | } 493 | }); 494 | 495 | a.join().unwrap(); 496 | b.join().unwrap(); 497 | } 498 | ``` 499 | 500 | 两个线程首先设置它们自己的原子布尔值到 true,以告知另一个线程它们正在获取 S,并且然后检查其它的原子变量布尔值,是否它们可以安全地获取 S,而不会导致数据竞争。 501 | 502 | 如果两个 store 操作都发生在任一 load 操作之前,则两个线程最终都无法访问 S。然而,两个线程都不可能访问 S 并导致未定义的行为,因为顺序一致的顺序保证了其中只有一个线程可以赢得竞争。在每个可能的单独全序中,第一个操作将是 store 操作,这阻止其他线程访问 S。 503 | 504 | 在实际情况中,几乎所有对 SeqCst 的使用都涉及一种类似的存储模式, store 操作在随后同一线程上的 load 操作之前必须成为全局可见的。对于这些情况,一个潜在的更有效的替代方案是将 relaxed 的操作与 SeqCst 屏障结合使用,我们接下来将探索。 505 | 506 | ## 屏障(Fence)[^2] 507 | 508 | (英文版本) 509 | 510 | 除了对原子变量的额外操作,我们还可以将内存排序应用于:原子屏障。 511 | 512 | `std::sync::atomic::fence` 函数表示一个*原子屏障*,它可以是一个 Release 屏障、一个 Acquire 屏障,或者两者都是(AcqRel 或 SeqCst)。SeqCst 屏障还参与到顺序一致性的全序中。 513 | 514 | 原子屏障允许你从原子操作中分离内存排序。如果你想要应用内存排序到多个操作这可能是有用的,或者你想要有条件地应用内存排序。 515 | 516 | 本质上,release-store 可以拆分成 release 屏障,然后是(relaxed)store,并且 acquire-load 可以拆分成(relaxed)load,然后是 acquire 屏障: 517 | 518 |
519 |
520 | release-acquire 关系的 store 操作, 521 |
522 |     a.store(1, Release);
523 | 可以由 release 屏障和随后的 relaxed store 组成: 524 |
525 |     fence(Release);
526 |     a.store(1, Relaxed);
527 |
528 |
529 | release-acquire 关系的 load 操作, 530 |
531 |       a.load(Acquire);
532 | 可以由 relaxed load 和随后的 acquire 屏障组成: 533 |
534 |     a.load(Relaxed);
535 |     fence(Acquire);
536 |
537 |
538 | 539 | 不过,使用单独的屏障可能会导致额外的处理器指令,这可能会略微降低效率。 540 | 541 | 更重要的是,与 release-store 或者 acquire-load 不同,屏障不会与任意单个原子变量捆绑。这意味着单个屏障可以立刻用于多个变量。 542 | 543 | 从形式上讲,如果 release 屏障在同一线程上的位置紧跟着任何一个原子操作,而该原子操作存储的值被我们要同步的 acquire 操作观察到,那么该 release 屏障可以代替 release 操作,并建立一个 happens-before 关系。同样地,如果一个 acquire 屏障在同一线程上的位置紧接着之前的任何一个原子操作,而该原子操作加载的值是由 release 操作存储的,那么该 acquire 屏障可以替代任何一个 acquire 操作。 544 | 545 | 综上所述,这意味着如果在 release 屏障之后的任何 store 操作被在 acquire 屏障之前的任何 load 操作所观察,那么在 release 屏障和 acquire 屏障之间将建立 happens-before 关系。 546 | 547 | 例如,假设我们有一个线程执行一个 release 屏障,然后对不同的变量执行三个原子 store 操作,另一个线程从这些相同的变量执行三个 load 操作,然后是一个 acquire 屏障,如下所示: 548 | 549 |
550 |
551 | 线程 1: 552 |
553 |     fence(Release);
554 |     A.store(1, Relaxed);
555 |     B.store(2, Relaxed);
556 |     C.store(3, Relaxed);
557 |
558 |
559 | 线程 2: 560 |
561 |     A.load(Relaxed);
562 |     B.load(Relaxed);
563 |     C.load(Relaxed);
564 |     fence(Acquire);
565 |
566 |
567 | 568 | 在这种情况下,如果线程 2 上的任意 load 操作从线程 1 上的相应 store 操作加载值,那么线程 1 上的 release 屏障和线程 2 上的 acquire 屏障之间将建立 happens-before 关系。 569 | 570 | 屏障不必直接在原子操作之前或者之后。屏障和原子操作之间可以进行任何事,包括控制流。这可以用于使屏障具有条件性,类似于「比较并交换」操作具有成功和失败的排序。 571 | 572 | 例如,如果我们从使用 acquire 内存排序的原子变量中加载指针,我们可以使用屏障仅当指针不是空的时候应用 acquire 内存排序: 573 | 574 |
575 |
576 | 使用 acquire-load: 577 |
let p = PTR.load(Acquire);
578 | if p.is_null() {
579 |     println!("no data");
580 | } else {
581 |     println!("data = {}", unsafe { *p });
582 | }
583 |
584 |
585 | 使用条件的 acquire 屏障: 586 |
let p = PTR.load(Relaxed);
587 | if p.is_null() {
588 |     println!("no data");
589 | } else {
590 |     fence(Acquire);
591 |     println!("data = {}", unsafe {*p });
592 | }
593 |
594 |
595 | 596 | 如果指针通常为空,这可能是有益的,在不需要时避免 acquire 内存排序。 597 | 598 | 让我们来看看一个更复杂的 release 和 acquire 屏障的用例: 599 | 600 | ```rust 601 | use std::sync::atomic::fence; 602 | 603 | static mut DATA: [u64; 10] = [0; 10]; 604 | 605 | const ATOMIC_FALSE: AtomicBool = AtomicBool::new(false); 606 | static READY: [AtomicBool; 10] = [ATOMIC_FALSE; 10]; 607 | 608 | fn main() { 609 | for i in 0..10 { 610 | thread::spawn(move || { 611 | let data = some_calculation(i); 612 | unsafe { DATA[i] = data }; 613 | READY[i].store(true, Release); 614 | }); 615 | } 616 | thread::sleep(Duration::from_millis(500)); 617 | let ready: [bool; 10] = std::array::from_fn(|i| READY[i].load(Relaxed)); 618 | if ready.contains(&true) { 619 | fence(Acquire); 620 | for i in 0..10 { 621 | if ready[i] { 622 | println!("data{i} = {}", unsafe { DATA[i] }); 623 | } 624 | } 625 | } 626 | } 627 | ``` 628 | 629 | > `std::array::from_fn` 是一种执行一定次数并将结果收集到数组中的简单方法。 630 | 631 | 在这个示例中,10 个线程做了一些计算,并存储它们的结果到一个(非原子)共享变量中。每个线程设置一个原子布尔值,以指示数据已经通过主线程准备好读取,使用一个普通的 release-store。主线程等待半秒,检查所有 10 个布尔值以查看哪些线程已完成,并打印任何准备好的结果。 632 | 633 | 主线程不使用 10 个 acquire-load 操作来读取布尔值,而是使用 relaxed 的操作和单个 acquire 屏障。它在读取数据之前执行屏障,但前提是有数据要读取。 634 | 635 | 虽然在这个特定的例子中,投入任何精力进行此类优化可能完全没有必要,但在构建高效的并发数据结构时,这种节省额外获取操作开销的模式可能很重要。 636 | 637 | `SeqCst` 屏障既是 release 屏障也是 acquire 屏障(就像 `AcqRel`),同时也是顺序一致性操作中单独全序的一部分。然而,只有屏障是全序的一部分,但它之前或之后的原子操作不必是总顺序的一部分。这意味着,与 release 或 acquire 操作不同,顺序一致性的操作不能拆分为 relaxed 操作和内存屏障。 638 | 639 |
640 |

编译器屏障

641 |

除了常规的原子屏障,Rust 标准库还提供了编译器屏障std::sync::atomic::compiler_fence。它的签名与我们上面讨论的这些常规 fence() 不同,但它的效果仅限于编译器。与原子屏障不同,例如,它并不会阻止处理器重排指令。在绝大多数屏障的用例中,编译器屏障是不够的。

642 | 643 |

在实现 Unix 信号处理程序或嵌入式系统上的中断时,可能会出现的用例。这些机制可以突然中断一个线程,暂时在同一处理器内核上执行一个不相关的函数。由于它发生在同一处理器内核上,处理器可能影响内存排序的常规方式不适用。(更多细节请参考第七章)在这种情况下,编译器屏障可能阻隔,这样可以节省一条指令并且希望提高性能。

644 | 645 |

另一用例涉及进程级内存屏障。这种技术超出了 Rust 内存模型的范畴,并且仅在某些操作系统上受支持:在 Linux 上通过 membarrier 系统调用,在 Windows 上使用 FlushProcessWriterBuffers 函数。它有效地允许一个线程强制向所有并发运行的线程注入(顺序一致性)原子屏障。这使得我们可以使用轻量级的编译器屏障和重型的进程级屏障替换两个匹配的屏障。如果轻量级屏障一侧的代码执行效率更高,这可以提高整体性能。(请参阅 crates.io 上的 membarrier crate 文档,了解更多详细信息和在 Rust 中使用这种屏障的跨平台方法。)

646 | 647 |

编译器屏障也可以是一个有趣的工具,用于探索处理器对内存排序的影响。

648 | 649 |

第七章“一个实验”中,我们将故意使用编译器屏障替换常规屏障。这将让我们在使用错误的内存排序时体验到处理器的微妙但潜在的灾难性影响。

650 |
651 | 652 | ## 常见的误解 653 | 654 | (英文版本) 655 | 656 | 围绕内存排序有很多误解。在我们结束本章之前,让我们回顾一下最常见的误解。 657 | 658 | > 误区:我需要强大的内存排序,以确保更改“立即”可见。 659 | 660 | 一个常见的误解是,使用像 Relaxed 这样的弱内存排序意味着对原子变量的更改可能永远不会到达另一个线程,或者只有在显著延迟之后才会到达。“Relaxed”这个名字可能会让它听起来像什么都没发生,直到有什么东西迫使硬件被唤醒并执行本该执行的操作。 661 | 662 | 事实是,内存模型并没有说任何关于时机的事情。它仅定义了某些事情发生的顺序;而不是你要等待多久。假设一台计算机需要数年时间才能将数据从一个线程传输到另一个线程,这完全无法使用,但可以完美地满足内存模型。 663 | 664 | 在现实生活中,内存排序是关于重新排序指令等事情,这通常以纳秒规模发生。更强的内存排序不会使你的数据传输速度更快;它甚至可能会减慢你的程序速度。 665 | 666 | > 误区:禁用优化意味着我不需要关心内存排序。 667 | 668 | 编译器和处理器在使事情按照我们预期的顺序发生方面起着作用。禁用编译器优化并不能禁用编译器中的每种可能的转换,也不能禁用处理器的功能,这些功能导致指令重新排序和类似的潜在问题行为。 669 | 670 | > 误区:使用不重新排序指令的处理器意味着我不需要关心内存排序。 671 | 672 | 一些简单的处理器,比如小型微控制器中的处理器,只有一个核心,每次只能执行一条指令,并且按顺序执行。然而,虽然在这类设备上,出现错误的内存排序导致实际问题的可能性较低,但编译器仍然可能基于错误的内存排序做出无效的假设,导致代码出错。此外,还需要认识到,即使处理器不会乱序执行指令,它仍可能具有其他与内存排序相关的特性。 673 | 674 | > 误区:Relaxed 的操作是免费的。 675 | 676 | 这是否成立取决于对“免费”一词的定义。的确,Relaxed 是最高效的内存排序,相比其他内存排序可以显著提升性能。事实上,对于所有现代平台,Relaxed load 和 store 操作编译成与非原子读写相同的处理器指令。 677 | 678 | 如果原子变量只在单个线程中使用,与非原子变量相比,速度上的差异很可能是因为编译器对非原子操作具有更大的自由度并更有效地进行优化。(编译器通常避免对原子变量进行大部分类型的优化。) 679 | 680 | 然而,从多个线程访问相同的内存通常比从单个线程访问要慢得多。当其他线程开始重复读取该变量时,持续写入原子变量的线程可能会遇到明显的减速,因为处理器核心和它们的缓存现在必须开始协作。 681 | 682 | 我们将在[第 7 章](./7_Understanding_the_Processor.md)中探讨这种效应。 683 | 684 | > 误区:顺序一致的内存排序是一个很好的默认值,并且总是正确的。 685 | 686 | 抛开性能问题,顺序一致性内存排序通常被视为默认选择的理想内存排序类型,因为它具有强大的保证。确实,如果任何其他内存排序是正确的,那么 `SeqCst` 也是正确的。这可能让人觉得 `SeqCst` 总是正确的。然而,可能并发算法本身就是不正确的,不论使用哪种内存排序。 687 | 688 | 更重要的是,在阅读代码时,`SeqCst` 基本上告诉读者:“该操作依赖于程序中每个单独 `SeqCst` 操作的全序”,这是一个极其广泛的声明。如果可能的话,使用较弱的内存排序往往会使相同的代码更容易进行审查和验证。例如,Release 明确告诉读者:“这与同一变量的 acquire 操作相关”,在形成对代码的理解时涉及的考虑要少得多。 689 | 690 | 建议将 SeqCst 看作是一个警示标识。在实际代码中看到它通常意味着要么涉及到复杂的情况,要么简单地说是作者没有花时间分析其与内存排序相关的假设,这两种情况都需要额外的审查。 691 | 692 | > 误区:顺序一致的内存排序可用于“release-store”或“acquire-load”。 693 | 694 | 虽然顺序一致性内存排序可以替代 Acquire 或 Release,但它并不能以某种方式创建 acquire-store 或 release-load。这些仍然是不存在的。Release 仅适用于 store 操作,而 acquire 仅适用于 load 操作。 695 | 696 | 例如,Release-store 与 SeqCst-store 不会形成任何 release-acquire 关系。如果你希望它们成为全局一致顺序的一部分,两个操作都必须使用 SeqCst。 697 | 698 | ## 总结 699 | 700 | (英文版本) 701 | 702 | * 所有的原子操作可能没有全局一致的顺序,因为不同的线程视角可能会以不同的顺序发生。 703 | * 然而,每个单独的原子变量都有它自己的*总修改顺序*,不管内存排序如何,所有线程都会达成一致意见。 704 | * 操作顺序是通过 *happens-before* 关系来定义的。 705 | * 在单个线程中,每个操作之间都会有一个 *happens-before* 关系。 706 | * 创建一个线程的操作在顺序上发生在该线程的所有操作之前。 707 | * 线程做的任何事情都会在 join 这个线程之前发生。 708 | * 解锁 mutex 的操作在顺序上发生在再次锁定 mutex 的操作之前。 709 | * 从 release 存储中以 acquire 加载值建立了一个 happens-before 关系。该值可以通过任意数量的「获取并修改」以及「比较并交换」操作修改。 710 | * 如果存在 consume-load,这将是 acquire-load 的轻量级版本。 711 | * 顺序一致的排序导致全局一致的操作顺序,但几乎从来都没有必要,并且会使代码审查更加复杂。 712 | * 屏障允许你组合多个操作的内存排序或有条件地应用内存排序。 713 | 714 |

715 | 下一篇,第四章:构建我们自己的自旋锁 716 |

717 | 718 | [^1]: 719 | [^2]: 720 | [^3]: 摘自 CPP 原子变量的内存排序翻译,请参考参见 721 | [^4]: 摘自 CPP 原子变量的内存排序翻译,请参考参见 722 | 723 | 参见: 724 | -------------------------------------------------------------------------------- /4_Building_Our_Own_Spin_Lock.md: -------------------------------------------------------------------------------- 1 | # 第四章:构建我们自己的自旋锁 2 | 3 | (英文版本) 4 | 5 | 对普通互斥锁(参见[第一章中的“锁:互斥锁和读写锁”](./1_Basic_of_Rust_Concurrency.md#锁互斥锁和读写锁))进行加锁时,如果互斥锁已经被锁定,线程将被置于睡眠状态。这避免在等待锁被释放时浪费资源。如果一个锁只会被短暂地持有,并且锁定它的线程可以在不同的处理器核心并发地运行,那么线程最好反复尝试锁定它而不真正进入睡眠态。 6 | 7 | 自旋锁是能够做到这一点的 mutex。试图锁定一个已经锁定的 mutex 将导致*忙碌循环*或者*自旋*:一遍又一遍的尝试。直到它成功。这可能浪费处理器周期,但有时在锁定时可以使延迟更低。 8 | 9 | > 在某些平台上,许多现实世界中的 mutex 实现,包括 `std::sync::Mutex`,在告诉操作系统将线程置于睡眠状态之前,短暂地表现得像一个自旋锁。这是为了将两者的优点结合起来,具体情况是否有益完全取决于特定的用例。 10 | 11 | 在该章节中,我们将建造我们自己的 `SpinLock` 类型,应用我们已经在第 [2](./2_Atomics.md) 章和第 [3](./3_Memory_Ordering.md) 章学习的,并且了解如何使用 Rust 的类型系统为我们的 SpinLock 用户提供安全且有用的接口。 12 | 13 | ## 一个最小实现 14 | 15 | (英文版本) 16 | 17 | 让我们从头实现这样的自旋锁。 18 | 19 | 最小的版本非常简单,如下: 20 | 21 | ```rust 22 | pub struct SpinLock { 23 | locked: AtomicBool, 24 | } 25 | ``` 26 | 27 | 我们需要的只是一个布尔值,指示自旋锁是否已锁定。我们使用一个*原子*布尔值,因为我们希望多个线程能够同时与它交互。 28 | 29 | 然后,我们只需要一个构造函数,以及锁定和解锁的方法: 30 | 31 | ```rust 32 | impl SpinLock { 33 | pub const fn new() -> Self { 34 | Self { locked: AtomicBool::new(false) } 35 | } 36 | 37 | pub fn lock(&self) { 38 | while self.locked.swap(true, Acquire) { 39 | std::hint::spin_loop(); 40 | } 41 | } 42 | 43 | pub fn unlock(&self) { 44 | self.locked.store(false, Release); 45 | } 46 | } 47 | ``` 48 | 49 | `locked` 的布尔值是从 false 开始的,`lock` 方法会将其替换为 true,如果它已经是 true,那么它将继续尝试,并且 `unlock` 方法仅将它设回 false。 50 | 51 | > 与其使用 `swap` 操作,我们也可以使用「比较并交换」操作去原子地检查布尔值是否是 false,如果是这种情况,将它设置为 true: 52 | > 53 | > ```rust 54 | > while self.locked.compare_exchange_weak( 55 | > false, true, Acquire, Relaxed).is_err() 56 | > ``` 57 | > 58 | > 这可能有点冗长,但是根据你的思维,这可能会更容易理解,因为它更容易地表述了可能失败和可能成功的情况。然而,它也导致了稍微不同的指令,正如我们将在[第七章](./7_Understanding_the_Processor.md)所看到的那样。 59 | 60 | 在 while 循环中,我们使用一个自旋循环提示,它告诉处理器我们正在自旋等待某些变化。在大多数平台上,该自旋导致处理器核心采取优化行为以应对这种情况。例如,它可能暂时地降低速度或优先处理其它有用的任务。然而,与 `thread::sleep` 或者 `thread::park` 等阻塞操作不同,自旋循环提示并不会调用操作系统的调用,将你的线程置于睡眠状态以便执行其它线程。 61 | 62 | > 总的来说,在自旋循环中包含这样的提示是一个好的主意。根据情况,在尝试再次访问原子变量之前,最好多次执行此提示。如果你关心最后几纳秒的性能并且想要找到最优的策略,你将不得不为你特定用例编写基准测试。不幸的是,正如我们将在[第 7 章](./7_Understanding_the_Processor.md)中看到的那样,此类基准测试的结果可能在很大程度上取决于硬件。 63 | 64 | 我们可以使用 acquire 和 release 内存排序去确保每个 `unlock()` 调用和随后的 `lock()` 调用都建立了一个 happens-before 关系。换句话说,为了确保锁定它后,我们可以安全地假设上次锁定期间的任何事情已经发生。这是 acquire 和 release 最经典的使用案列:获取和释放一个锁。 65 | 66 | 图 4-1 展示了使用 `SpinLock` 来保护对一些共享数据的访问情况,其中两个线程同时尝试获取锁。请注意,第一个线程上的解锁操作与第二个线程上的锁定操作形成 happens-before 关系,这确保了线程不能并发地访问数据。 67 | 68 | ![ ](https://github.com/rustcc/Rust_Atomics_and_Locks/raw/main/picture/raal_0401.png) 69 | 图 4-1。在使用 `SpinLock` 保护对某些共享数据访问的两个线程之间的 happens-before 关系。 70 | 71 | ## 一个不安全的自旋锁 72 | 73 | (英文版本) 74 | 75 | 我们上面实现的 SpinLock 类型有一个完全安全地接口,它并不会引起任何未定义行为。然而,在大多数的使用案列中,它将被用于保护共享变量的可变性,这意味着用于将仍然使用一个不安全的、未检查的代码。 76 | 77 | 为了提供一个简单的接口,我们可以改变 `lock` 方法为直接提供受锁保护数据的独占的引用(`&mut T`),因为在大多数情况下,锁操作保证了可以安全地假设具有独占访问权限。 78 | 79 | 为了能够做到这一点,我们必须将类型更改为更加通用,而不是受保护的数据类型,并且添加一个字段持有数据。因为即使自旋锁是共享的,数据也是可变的(或者独占访问),我们需要去使用内部可变性(参见[第 1 章中的“内部可变性”](./1_Basic_of_Rust_Concurrency.md#内部可变性)),为此我们将使用 `UnsafeCell`: 80 | 81 | ```rust 82 | use std::cell::UnsafeCell; 83 | 84 | pub struct SpinLock { 85 | locked: AtomicBool, 86 | value: UnsafeCell, 87 | } 88 | ``` 89 | 90 | 作为一种预防措施,UnsafeCell 没有实现 `Sync`,这意味着我们的类型现在不再可以在线程之间共享,使其变得毫无用处。为了修复它,我们需要向编译器保证我们的类型实际上可以在线程之间共享是安全的。然而,因为锁可以用于在线程之间发送类型为 T 的值,我们将这个承诺限制为哪些类型可以在线程之间安全发送。因此,我们(不安全地)为所有实现 `Send` 的 T 实现 `SpinLock` 的 `Sync`,如下所示: 91 | 92 | ```rust 93 | unsafe impl Sync for SpinLock where T: Send {} 94 | ``` 95 | 96 | 注意,我们并不需要去要求 T 是 `Sync`,由于我们的 `SpinLock` 一次仅允许一个线程访问它保护的 T。只有当我们同时允许多个线程访问时,就像读写锁对 reader 所做的那样,我们(另外)才需要 `T: Sync`。 97 | 98 | 下一步,现在我们的新函数需要接收一个 T 类型的值来初始化 `UnsafeCell`: 99 | 100 | ```rust 101 | impl SpinLock { 102 | pub const fn new(value: T) -> Self { 103 | Self { 104 | locked: AtomicBool::new(false), 105 | value: UnsafeCell::new(value), 106 | } 107 | } 108 | 109 | // … 110 | } 111 | ``` 112 | 113 | 然后我们进入有趣的部分:锁定和解锁。我们做这一切的原因,是为了能够从 `lock()` 中返回 `&mut T`,例如,这样用户在使用我们的锁来保护它们的数据时,并不要求写不安全、未检查的代码。这意味着,我们现在的 `lock` 实现必须使用一个不安全的代码。`UnsafeCell` 可以通过其 `get()` 方法向我们提供指向其内容(`*mut T`)的原始指针,我们可以使用不安全块转换到一个引用,如下所示: 114 | 115 | ```rust 116 | pub fn lock(&self) -> &mut T { 117 | while self.locked.swap(true, Acquire) { 118 | std::hint::spin_loop(); 119 | } 120 | unsafe { &mut *self.value.get() } 121 | } 122 | ``` 123 | 124 | 由于 `lock` 函数的函数签名在其输入和输出都包含引用,`&self` 和 `&mut T` 的生命周期都已经被省略并假定为相同的生命周期。(参见《Rust Book》中的“Chapter 10: Generic Types, Traits, and Lifetimes”的“Lifetime Elision”一节)。我们可以通过手动书写来明确这些生命周期,如下所示: 125 | 126 | ```rust 127 | pub fn lock<'a>(&'a self) -> &'a mut T { … } 128 | ``` 129 | 130 | 这清楚的表明,返回引用的生命周期与 `&self` 的生命周期相同。这意味着我们已经声称,只要锁本身存在,返回的引用就是有效的。 131 | 132 | 如果我们假装 `unlock()` 不存在,这将是完全安全和健全的接口。SpinLock 可以被锁定,导致一个 `&mut T`,并且然后不再被再次锁定,这保证了这个独占引用确实是独占的。 133 | 134 | 然而,如果我们尝试重新**引入** `unlock()` 方法,我们需要一种方式去限制返回引用的生命周期,直到下一次调用 `unlock()`。如果编译器理解英语,或者它应该这样工作: 135 | 136 | ```rust 137 | pub fn lock<'a>(&self) -> &'a mut T 138 | where 139 | 'a ends at the next call to unlock() on self, 140 | even if that's done by another thread. 141 | Oh, and it also ends when self is dropped, of course. 142 | (Thanks!) 143 | { … } 144 | ``` 145 | 146 | 不幸的是,这并不是有效的 Rust。我们必须努力向用户解释这个限制,而不是向编译器解释。为了将责任转移到用户身上,我们将 `unlock` 函数标记为不安全,并给他们留下一张纸条,解释他们需要做什么来保持健全: 147 | 148 | ```rust 149 | /// 安全性:来自 lock() 的 &mut T 必须消失 150 | /// (并且通过引用该 T 周围的字段来防止欺骗!) 151 | pub unsafe fn unlock(&self) { 152 | self.locked.store(false, Release); 153 | } 154 | ``` 155 | 156 | ## 使用锁守卫的安全接口 157 | 158 | (英文版本) 159 | 160 | 为了能够提供一个完全安全地接口,我们需要将解锁操作绑定到 `&mut T` 的末尾。我们可以通过将此引用包装成我们自己的类型来做到这一点,该类型的行为类似于引用,但也实现了 Drop trait,以便在它被丢弃时做一些事情。 161 | 162 | 这一类型通常被称为*守卫*(guard),因为它有效地守卫了锁的状态,并且对该状态负责,直到它被丢弃。 163 | 164 | 我们的 `Guard` 类型将仅包含对 SpinLock 的引用,以便它既可以访问 UnsafeCell,也可以稍后重置 AtomicBool: 165 | 166 | ```rust 167 | pub struct Guard { 168 | lock: &SpinLock, 169 | } 170 | ``` 171 | 172 | 然而,如果我们尝试编译它,编译器将告诉我们: 173 | 174 | ```txt 175 | error[E0106]: missing lifetime specifier 176 | --> src/lib.rs 177 | | 178 | | lock: &SpinLock, 179 | | ^ expected named lifetime parameter 180 | | 181 | help: consider introducing a named lifetime parameter 182 | | 183 | ~ pub struct Guard<'a, T> { 184 | | ^^^ 185 | ~ lock: &'a SpinLock, 186 | | ^^ 187 | | 188 | ``` 189 | 190 | 显然,这不是一个可以淘汰生命周期的地方。我们必须明确表示,引用的生命周期有限,正如编译器所建议的那样: 191 | 192 | ```rust 193 | pub struct Guard<'a, T> { 194 | lock: &'a SpinLock, 195 | } 196 | ``` 197 | 198 | 这保证了 Guard 不能超出 SpinLock 的生命周期。 199 | 200 | 下一步,我们在我们的 SpinLock 上改变 lock 方法,以返回 Guard: 201 | 202 | ```rust 203 | pub fn lock(&self) -> Guard { 204 | while self.locked.swap(true, Acquire) { 205 | std::hint::spin_loop(); 206 | } 207 | Guard { lock: self } 208 | } 209 | ``` 210 | 211 | 我们的 Guard 类型没有构造函数,其字段是私有的,因此这是用户获得 Guard 的唯一方法。因此,我们可以有把握地假设 Guard 的存在意味着 SpinLock 已被锁定。 212 | 213 | 为了使 `Guard` 行为类似一个(独占)引用,透明地允许访问 T,我们必须实现以下特殊的 Deref 和 DerefMut trait: 214 | 215 | ```rust 216 | use std::ops::{Deref, DerefMut}; 217 | 218 | impl Deref for Guard<'_, T> { 219 | type Target = T; 220 | fn deref(&self) -> &T { 221 | // 安全性:Guard 的 存在 222 | // 保证了我们已经独占地锁定这个锁 223 | unsafe { &*self.lock.value.get() } 224 | } 225 | } 226 | 227 | impl DerefMut for Guard<'_, T> { 228 | fn deref_mut(&mut self) -> &mut T { 229 | // 安全性:Guard 的存在 230 | // 保证了我们已经独占地锁定这个锁 231 | unsafe { &mut *self.lock.value.get() } 232 | } 233 | } 234 | ``` 235 | 236 | 最后一步,我们为 Guard 实现 Drop,允许我们完全地引出不安全的 `unlock()` 方法: 237 | 238 | ```rust 239 | impl Drop for Guard<'_, T> { 240 | fn drop(&mut self) { 241 | self.lock.locked.store(false, Release); 242 | } 243 | } 244 | ``` 245 | 246 | 就这样,通过 Drop 和 Rust 类型系统的魔力,我们为我们的 SpinLock 类型提供了一个完全安全(和有用的)接口。 247 | 248 | 让我们尝试使用它: 249 | 250 | ```rust 251 | fn main() { 252 | let x = SpinLock::new(Vec::new()); 253 | thread::scope(|s| { 254 | s.spawn(|| x.lock().push(1)); 255 | s.spawn(|| { 256 | let mut g = x.lock(); 257 | g.push(2); 258 | g.push(2); 259 | }); 260 | }); 261 | let g = x.lock(); 262 | assert!(g.as_slice() == [1, 2, 2] || g.as_slice() == [2, 2, 1]); 263 | } 264 | ``` 265 | 266 | 上面的程序展示了我们的 `SpinLock` 是多么容易使用。多亏了 `Deref` 和 `DerefMut`,我们可以直接在 guard 上调用 `Vec::push` 方法。多亏了 `Drop`,我们不必担心解锁。 267 | 268 | 通过调用 `drop(g)` 来丢弃 guard,也可以明确地解锁。如果你尝试过早地解锁,你将看见 guard 正在做它的工作时,发生编译器错误。例如,如果你在两个 `push(2)` 行之间插入 `drop(g);`,第二个 push 将无法编译,因为你此时已经丢弃 g 了: 269 | 270 | ```txt 271 | error[E0382]: borrow of moved value: `g` 272 | --> src/lib.rs 273 | | 274 | | drop(g); 275 | | - value moved here 276 | | g.push(2); 277 | | ^^^^^^^^^ value borrowed here after move 278 | ``` 279 | 280 | 多亏了 Rust 的类型系统,我们可以放心,在我们运行程序之前,这样的错误就已经被发现了。 281 | 282 | ## 总结 283 | 284 | (英文版本) 285 | 286 | * 自旋锁是在等待时忙碌循环或自旋的 mutex。 287 | * 自旋可以**减少**延迟,但也可能浪费时钟周期并降低性能。 288 | * 自旋循环提示(`spin::hint::spin_loop()`)可以用于通知处理器自旋循环,这可能**增加**它的效率。 289 | * `SpinLock` 只需使用 `AtomicBool` 和 `UnsafeCell` 即可实现,后者是*内部可变性*所必需的(见[第 1 章中的“内部可变性”](./1_Basic_of_Rust_Concurrency.md#内部可变性))。 290 | * 在解锁和锁定之间的 *happens-before 关系*是防止*数据竞争*的必要条件,否则会导致未定义行为。 291 | * *Acquire* 和 *Release* 内存排序对这个用例是极合适的。 292 | * 当做出必要的未检查的假设以避免未定义的行为时,可以通过将函数标记为不安全来将责任转移到调用者。 293 | * `Deref` 和 `DerefMut` trait 可用于使类型像引用一样,透明地提供对另一个对象的访问。 294 | * `Drop` trait 可以用于在对象被丢弃时,做一些事情,例如当它超出作用域或者它被传递给 `drop()`。 295 | * *锁守卫*是一种特殊类型的有用设计模式,它被用于表示对锁定的锁的(安全)访问。由于 `Deref` trait,这种类型通常与引用的行为相似,并通过 `Drop` trait 实现自动解锁。 296 | 297 |

298 | 下一篇,第五章:构建我们自己的 Channel 299 |

300 | -------------------------------------------------------------------------------- /5_Building_Our_Own_Channels.md: -------------------------------------------------------------------------------- 1 | # 第五章:构建我们自己的 Channel 2 | 3 | (英文版本) 4 | 5 | *Channel* 可以被用于在线程之间发送数据,并且它有很多变体。一些 channel 仅能在一个发送者和一个接收者之间使用,而另一些可以在任意数量的线程之间发送,或者甚至允许多个接收者。一些 channel 是阻塞的,这意味着接收(有时也包括发送)是一个阻塞操作,这会使线程进入睡眠状态,直到你的操作完成。一些 channel 针对吞吐量进行优化,而另一些针对低延迟进行优化。 6 | 7 | 这些变体是无穷尽的,没有一种通用版本在所有场景都适合的。 8 | 9 | 在该章节,我们将实现一个相对简单的 channel,不仅可以探索更多的原子应用,同时也可以了解如何在 Rust 类型系统中捕获我们的需求和假设。 10 | 11 | ## 一个简单的以 mutex 为基础的 Channel 12 | 13 | (英文版本) 14 | 15 | 一个基础的 channel 实现并不需要任何关于原子的知识。我们可以接收 `VecDeque`,它根本上是一个 `Vec`,允许在两端高效地添加和移除元素,并使用 Mutex 保护它,以允许多个线程访问。然后,我们使用 `VecDeque` 作为已发送但尚未接受数据的消息队列。任何想要发送消息的线程只需要将其添加到队列的末尾,而任何想要接受消息的线程只需从队列的前端删除一个消息。 16 | 17 | 还有一点需要补充,用于将接收操作阻塞的 Condvar(参见[第一章“条件变量”](./1_Basic_of_Rust_Concurrency.md#条件变量)),当有新的消息,它会通知正在等待的接收者。 18 | 19 | 这样做的实现可能非常简短且相对直接,如下所示: 20 | 21 | ```rust 22 | pub struct Channel { 23 | queue: Mutex>, 24 | item_ready: Condvar, 25 | } 26 | 27 | impl Channel { 28 | pub fn new() -> Self { 29 | Self { 30 | queue: Mutex::new(VecDeque::new()), 31 | item_ready: Condvar::new(), 32 | } 33 | } 34 | 35 | pub fn send(&self, message: T) { 36 | self.queue.lock().unwrap().push_back(message); 37 | self.item_ready.notify_one(); 38 | } 39 | 40 | pub fn receive(&self) -> T { 41 | let mut b = self.queue.lock().unwrap(); 42 | loop { 43 | if let Some(message) = b.pop_front() { 44 | return message; 45 | } 46 | b = self.item_ready.wait(b).unwrap(); 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | 注意,我们并没有使用任意的原子操作或者不安全代码,也不需要考虑 `Send` 或者 `Sync`。编译器理解 Mutex 的接口以及保证该提供什么类型,并且会隐式地理解,如果 `Mutex` 和 Condvar 都可以在线程之间安全共享,那么我们的 `Channel` 也可以这么做。 53 | 54 | 我们的 `send` 函数锁定 mutex,然后从队列的末尾推入消息,并且使用条件变量在解锁队列后直接通知可能等待的接收者。 55 | 56 | `receive` 函数也锁定 mutex,然后从队列的首部弹出消息,但如果仍然没有可获得的消息,则会使用条件变量去等待。 57 | 58 | > 记住,`Condvar::wait` 方法将在等待时解锁 Mutex,并在返回之前重新锁定它。因此,我们的 `receive` 函数将不会在等待时锁定 mutex。 59 | 60 | 尽管这个 channel 在使用上是非常灵活的,因为它允许任意数量的发送和接收线程,但在很多情况下,它的实现远非最佳。即使有大量的消息准备好被接收,任意的发送或者接收操作将短暂地阻塞任意其它的发送或者接收操作,因为它们必须都锁定相同的 mutex。如果 `VecDeque::push` 必须增加 VecDeque 的容量时,所有的发送和接收线程将不得不等待该线程完成重新分配容量,这在某些情况下是不可接受的。 61 | 62 | 另一个可能不可取的属性是,该 channel 的队列可能会无限制地增长。没有什么能阻止发送者以比接收者更高的速度持续发送新消息。 63 | 64 | ## 一个不安全的一次性 Channel 65 | 66 | (英文版本) 67 | 68 | channel 的各种用例几乎是无止尽的。然而,在本章的剩余部分,我们将专注于一种特定类型的用例:恰好从一个线程向另一个线程发送一条消息。为此类用例设计的 channel 通常被称为 *一次性*(one-shot)channel。 69 | 70 | 我们采用上述基于 `Mutex` 的实现,并且将 `VecDeque` 替换为 `Option`,从而将队列的容量减小到恰好一个消息。这样可以避免内存浪费,但仍然会存在使用 Mutex 的一些缺点。我们可以通过使用原子操作从头构建我们自己的一次性 channel 来避免这个问题。 71 | 72 | 首先,让我们构建一个最小化的一次性 channel 实现,不需要考虑它的接口。在本章的稍后,我们将探索如何改进其接口以及如何与 Rust 类型相结合,为 channel 的用于提供愉快的体验。 73 | 74 | 我们需要开始的工具基本上与我们在[第四章](./4_Building_Our_Own_Spin_Lock.md)使用的 `SpinLock` 基本相同:一个用于存储的 `UnsafeCell` 和用于指示状态的 `AtomicBool`。在该示例中,我们使用原子布尔值去指示消息是否准备好用于消费。 75 | 76 | 在发送消息之前,channel 是“空的”并且不包含任何类型为 T 的消息。我们可以在 cell 中使用 `Option`,以允许 T 缺失。然而,这可能会浪费宝贵的内存空间,因为我们的原子布尔值已经告诉我们是否有消息。相反,我们可以使用 `std::mem::MaybeUninit`,它本质上是裸露的 `Option` 的不安全版本:它要求用户手动跟踪其是否已初始化,几乎整个接口都是不安全的,因为它不能执行自己的检查。 77 | 78 | 综合来看,我们从这个结构体定义开始我们的第一次尝试: 79 | 80 | ```rust 81 | use std::mem::MaybeUninit; 82 | 83 | pub struct Channel { 84 | message: UnsafeCell>, 85 | ready: AtomicBool, 86 | } 87 | ``` 88 | 89 | 就像我们的 `SpinLock` 一样,我们需要告诉编译器,我们的 channel 在线程之间共享是安全的,或者至少只要 T 是 `Send` 的: 90 | 91 | ```rust 92 | unsafe impl Sync for Channel where T: Send {} 93 | ``` 94 | 95 | 一个新的 channel 是空的,将 `ready` 设置为 false,并且消息仍然没有初始化: 96 | 97 | ```rust 98 | impl Channel { 99 | pub const fn new() -> Self { 100 | Self { 101 | message: UnsafeCell::new(MaybeUninit::uninit()), 102 | ready: AtomicBool::new(false), 103 | } 104 | } 105 | 106 | // … 107 | } 108 | ``` 109 | 110 | 要发送消息,它首先需要存储在 cell 中,之后我们可以通过将 ready 标识设置为 true 来将其释放给接收者。试图做这个超过一次是危险的,因为设置 ready 标识后,接收者可能在任意时刻读取消息,这可能会与第二次发送消息产生数据竞争。目前,我们通过使方法不安全并为它们留下备注,将此作为用户的责任: 111 | 112 | ```rust 113 | /// 安全性:仅能调用一次! 114 | pub unsafe fn send(&self, message: T) { 115 | (*self.message.get()).write(message); 116 | self.ready.store(true, Release); 117 | } 118 | ``` 119 | 120 | 在上面这个片段中,我们使用 `UnsafeCell::get` 方法去获取指向 `MaybeUninit` 的指针,并且通过不安全地解引用它来调用 `MaybeUninit::write` 进行初始化。当错误使用时,这可能导致未定义行为,但我们将这个责任转移到了调用方身上。 121 | 122 | 对于内存排序,我们需要使用 release 排序,因为原子的存储有效地将消息释放给接收者。这确保了如果接收线程从 `self.ready` 以 acquire 排序加载 true,则消息的初始化将从接受线程的角度完成。 123 | 124 | 对于接收,我们暂时不会提供阻塞的接口。相反,我们将提供两个方法:一个用于检查是否有可用消息,另一个用于接收消息。我们将让我们的 channel 用户决定是否使用[线程阻塞](./1_Basic_of_Rust_Concurrency.md#线程阻塞)的方法来阻塞。 125 | 126 | 以下是完成此版本我们 channel 的最后两种方法: 127 | 128 | ```rust 129 | pub fn is_ready(&self) -> bool { 130 | self.ready.load(Acquire) 131 | } 132 | 133 | /// 安全性:仅能调用一次, 134 | /// 并且仅在 is_ready() 返回 true 之后调用! 135 | pub unsafe fn receive(&self) -> T { 136 | (*self.message.get()).assume_init_read() 137 | } 138 | ``` 139 | 140 | 虽然 `is_ready` 方法可以始终地安全调用,但是 `receive` 方法使用了 `MaybeUninit::assume_init_read()`,这不安全地假设它已经被初始化,且不会用于生成非 `Copy` 对象的多个副本。就像 `send` 方法一样,我们只需通过将函数本身标记为不安全来将这个问题交给用户解决。 141 | 142 | 结果是一个在技术上可用的 channel,但它用起来不便并且通常令人失望。如果正确使用,它会按预期进行操作,但有很多微妙的方式去错误地使用它。 143 | 144 | 多次调用 send 可能会导致数据竞争,因为第二个发送者在接收者尝试读取第一条消息时可能正在覆盖数据。即使接收操作得到了正确的同步,从多个线程调用 send 可能会导致两个线程尝试并发地写入 cell,再次导致数据竞争。此外,多次调用 `receive` 会导致获取两个消息的副本,即使 T 不实现 `Copy` 并且因此不能安全地进行复制。 145 | 146 | 更微妙的问题是我们的 Channel 缺乏 `Drop` 实现。`MaybeUninit` 类型不会跟踪它是否已经初始化,因此它在被丢弃时不会自动丢弃其内容。这意味着如果发送了一条消息但从未被接收,该消息将永远不会被释放。这并不是不正确的,但仍然是要避免。在 Rust 中,泄漏被普遍认为是安全的,但通常只有作为另一个泄漏的后果才是可接受的。例如,泄漏 Vec 的内存也会泄漏其内容,但正常使用 Vec 不会导致任何内存泄漏。 147 | 148 | 由于我们让用户对一切负责,不幸的事故只是时间问题。 149 | 150 | ## 通过运行时检查来达到安全 151 | 152 | (英文版本) 153 | 154 | 为了提供更安全的接口,我们可以增加一些检查,以确保误用会导致 panic 并显示清晰的错误信息,这比未定义行为要好得多。 155 | 156 | 让我们在消息准备好之前调用 `receive` 方法的问题开始处理。这个问题很容易解决,我们只需要在尝试读消息之前让 receive 方法验证 ready 标识即可: 157 | 158 | ```rust 159 | /// 如果仍然没有消息可获得,panic。 160 | /// 161 | /// 提示,首先使用 `is_ready` 检查。 162 | /// 163 | /// 安全地:仅能调用一次。 164 | pub unsafe fn receive(&self) -> T { 165 | if !self.ready.load(Acquire) { 166 | panic!("no message available!"); 167 | } 168 | (*self.message.get()).assume_init_read() 169 | } 170 | ``` 171 | 172 | 该函数仍然是不安全的,因为用户仍然需要确保只调用一次,但未能首先检查 `is_ready()` 不再导致未定义行为。 173 | 174 | 因为我们现在在 `receive` 方法里有一个 `ready` 标识的 acquire-load 操作,其提供了必要的同步,我们可以在 `is_ready` 中使用 Relaxed 内存排序,因为该操作现在仅用于指示目的: 175 | 176 | ```rust 177 | pub fn is_ready(&self) -> bool { 178 | self.ready.load(Relaxed) 179 | } 180 | ``` 181 | 182 | > 记住,ready 上的总修改顺序(参见[第三章的“Relaxed 排序”](./3_Memory_Ordering.md#relaxed-排序))保证了从 `is_ready` 加载 true 之后,receive 也能看到 true。无论 is_ready 使用的内存排序如何,都不会出现 `is_ready` 返回 true,`receive()` 仍然出现 panic 的情况。 183 | 184 | 下一个要解决的问题是,当调用 receive 不止一次时会发生什么。通过在接收方法中将 `ready` 标识设置回 false,我们也可以很容易地导致 panic,例如: 185 | 186 | ```rust 187 | /// 如果仍然没有消息可获得, 188 | /// 或者消息已经被消费 panic。 189 | /// 190 | /// 提示,首先使用 `is_ready` 检查。 191 | pub fn receive(&self) -> T { 192 | if !self.ready.swap(false, Acquire) { 193 | panic!("no message available!"); 194 | } 195 | // Safety: We've just checked (and reset) the ready flag. 196 | unsafe { (*self.message.get()).assume_init_read() } 197 | } 198 | ``` 199 | 200 | 我们仅是将 load 操作更改为 swap 操作(交换的值为 `false`),突然之间,receive 方法在任何情况下都可以安全地调用。该函数不再标记为不安全。我们现在承担了不安全代码的责任,而不是让用户负责一切,从而减轻了用户的压力。 201 | 202 | 对于 send,事情稍微复杂一点。为了阻止多个 send 调用同时访问 cell,我们需要知道是否另一个 send 调用已经开始。ready 标识仅告诉我们是否另一个 send 调用已经完成,所以这还不够。 203 | 204 | 让我们增加第二个标识,命名为 `in_use`,以指示该 channel 是否已经在使用: 205 | 206 | ```rust 207 | pub struct Channel { 208 | message: UnsafeCell>, 209 | in_use: AtomicBool, // 新增! 210 | ready: AtomicBool, 211 | } 212 | 213 | impl Channel { 214 | pub const fn new() -> Self { 215 | Self { 216 | message: UnsafeCell::new(MaybeUninit::uninit()), 217 | in_use: AtomicBool::new(false), // 新增! 218 | ready: AtomicBool::new(false), 219 | } 220 | } 221 | 222 | //… 223 | } 224 | ``` 225 | 226 | 现在我们需要做的就是在访问 cell 之前,在 send 方法中,将 `in_use` 设置为 true,如果它已经由另一个线程设置,则 panic: 227 | 228 | ```rust 229 | /// 当尝试发送不止一次消息时,Panic。 230 | pub fn send(&self, message: T) { 231 | if self.in_use.swap(true, Relaxed) { 232 | panic!("can't send more than one message!"); 233 | } 234 | unsafe { (*self.message.get()).write(message) }; 235 | self.ready.store(true, Release); 236 | } 237 | ``` 238 | 239 | 我们可以为原子 swap 操作使用 relaxed 内存排序,因为 `in_use` 的*总修改顺序*(参见[第三章“Relaxed 排序”](./3_Memory_Ordering.md#relaxed-排序))保证了在 in_use 上只会有一个 swap 操作返回的 false,而这是 send 方法尝试访问 cell 的唯一情况。 240 | 241 | 现在我们拥有了一个完全安全的接口,尽管还有一个问题未解决。最后一个问题出现在发送一个永远不会被接收的消息时:它将从不会被丢弃。虽然这不会导致未定义行为,并且在安全代码中是允许的,但确实应该避免这种情况。 242 | 243 | 由于我们在 receive 方法中重置了 ready 标识,修复这个问题很容易:ready 标识指示是否在 cell 中尚未接受的消息需要被丢弃。 244 | 245 | 在我们的 Channel 的 Drop 实现中,我们不需要使用一个原子操作去检查原子 ready 标识,因为只有对象完全被正在丢弃它的线程所拥有的时候,且没有任何未解除借用的情况下,才能丢弃一个对象。这意味着,我们可以使用 `AtomicBool::get_mut` 方法,它接受一个独占引用(`&mut self`),以证明原子访问是不必要的。对于 UnsafeCell 也是一样,通过 `UnsafeCell::get_mut` 方法来来获取独占引用。 246 | 247 | 使用它,这是我们完全安全且不泄漏的 channel 的最后一部分: 248 | 249 | ```rust 250 | impl Drop for Channel { 251 | fn drop(&mut self) { 252 | if *self.ready.get_mut() { 253 | unsafe { self.message.get_mut().assume_init_drop() } 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | 我们试试吧! 260 | 261 | 由于我们的 channel 仍没有提供一个阻塞的接口,我们将手动地使用线程阻塞去等待消息。只要没有消息准备好,接收线程将 `park()` 自身,并且发送线程将在发送东西后,立刻 `unpark()` 接收者。 262 | 263 | 这里是一个完整的测试程序,通过我们的 `Channel` 从第二个线程发送字符串字面量“hello world”到主线程: 264 | 265 | ```rust 266 | fn main() { 267 | let channel = Channel::new(); 268 | let t = thread::current(); 269 | thread::scope(|s| { 270 | s.spawn(|| { 271 | channel.send("hello world!"); 272 | t.unpark(); 273 | }); 274 | while !channel.is_ready() { 275 | thread::park(); 276 | } 277 | assert_eq!(channel.receive(), "hello world!"); 278 | }); 279 | } 280 | ``` 281 | 282 | 该程序编译、运行和干净地退出,表明我们的 Channel 正常工作。 283 | 284 | 如果我们复制了 send 行,我们也可以在运行中看到我们的安全检查,当运行程序时,产生以下 panic: 285 | 286 | ```txt 287 | thread '' panicked at 'can't send more than one message!', src/main.rs 288 | ``` 289 | 290 | 尽管 panic 程序并不出色,但是程序可靠的 panic 比可能的未定义行为错误好太多。 291 | 292 |
293 |

为 Channel 状态使用单原子

294 | 295 |

如果你对 channel 实现还不满意,这里有一个微妙的变体,可以节省一字节的内存。

296 | 297 |

我们使用单个原子 AtomicU8 表示所有 4 个状态,而不是使用两个分开的布尔值去表示 channel 的状态。我们必须使用 compare_exchange 来原子地检查 channel 是否处于预期状态,并将其更改为另一个状态,而不是原子交换布尔值。

298 | 299 |
const EMPTY: u8 = 0;
300 | const WRITING: u8 = 1;
301 | const READY: u8 = 2;
302 | const READING: u8 = 3;
303 | 
304 | pub struct Channel<T> {
305 |     message: UnsafeCell<MaybeUninit<T>>,
306 |     state: AtomicU8,
307 | }
308 | 
309 | unsafe impl<T: Send> Sync for Channel<T> {}
310 | 
311 | impl<T> Channel<T> {
312 |     pub const fn new() -> Self {
313 |         Self {
314 |             message: UnsafeCell::new(MaybeUninit::uninit()),
315 |             state: AtomicU8::new(EMPTY),
316 |         }
317 |     }
318 | 
319 |     pub fn send(&self, message: T) {
320 |         if self.state.compare_exchange(
321 |             EMPTY, WRITING, Relaxed, Relaxed
322 |         ).is_err() {
323 |             panic!("can't send more than one message!");
324 |         }
325 |         unsafe { (*self.message.get()).write(message) };
326 |         self.state.store(READY, Release);
327 |     }
328 | 
329 |     pub fn is_ready(&self) -> bool {
330 |         self.state.load(Relaxed) == READY
331 |     }
332 | 
333 |     pub fn receive(&self) -> T {
334 |         if self.state.compare_exchange(
335 |             READY, READING, Acquire, Relaxed
336 |         ).is_err() {
337 |             panic!("no message available!");
338 |         }
339 |         unsafe { (*self.message.get()).assume_init_read() }
340 |     }
341 | }
342 | 
343 | impl<T> Drop for Channel<T> {
344 |     fn drop(&mut self) {
345 |         if *self.state.get_mut() == READY {
346 |             unsafe { self.message.get_mut().assume_init_drop() }
347 |         }
348 |     }
349 | }
350 | 351 |
352 | 353 | ## 通过类型来达到安全 354 | 355 | (英文版本) 356 | 357 | 尽管我们已经成功地保护了我们 Channel 的用户免受未定义行为的问题,但是如果它们偶尔地不正确使用它,它们仍然有 panic 的风险。理想情况下,编译器将在程序运行之前检查正确的用法并指出滥用。 358 | 359 | 让我们来看看调用 send 或 receive 不止一次的问题。 360 | 361 | 362 | 为了防止函数被多次调用,我们可以让它*按值*接受参数,对于非 `Copy` 类型,这将消耗对象。对象被消耗或移动后,它会从调用者那里消失,防止它再次被使用。 363 | 364 | 通过将调用 send 或 receive 表示的能力作为单独的(非 `Copy`)类型,并在执行操作时消费对象,我们可以确保每个操作只能发生一次。 365 | 366 | 这给我们带来了以下接口设计,而不是单个 `Channel` 类型,一个 channel 由一对 `Sender` 和 `Receiver` 表示,它们各自都有以值接收 `self` 的方法: 367 | 368 | ```rust 369 | pub fn channel() -> (Sender, Receiver) { … } 370 | 371 | pub struct Sender { … } 372 | pub struct Receiver { … } 373 | 374 | impl Sender { 375 | pub fn send(self, message: T) { … } 376 | } 377 | 378 | impl Receiver { 379 | pub fn is_ready(&self) -> bool { … } 380 | pub fn receive(self) -> T { … } 381 | } 382 | ``` 383 | 384 | 用户可以通过调用 `channel()` 创建一个 channel,这将给他们一个 Sender 和一个 Receiver。它们可以自由地传递每个对象,将它们移动到另一个线程,等等。然而,它们最终不能获得其中任何一个的多个副本,这保证了 send 和 receive 仅被调用一次。 385 | 386 | 为了实现这一点,我们需要为我们的 UnsafeCell 和 AtomicBool 找到一个位置。之前,我们仅有一个具有这些字段的结构体,但是现在我们有两个单独的结构体,每个结构体都可能存在更长的时间。 387 | 388 | 389 | 因为 sender 和 receiver 将需要共享这些变量的所有权,我们将使用 Arc([第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数))为我们提供引用计数共享内存分配,我们将在其中存储共享的 Channel 对象。正如以下展示的,Channel 类型不必是公共的,因为它的存在是与用户无关的细节。 390 | 391 | ```rust 392 | pub struct Sender { 393 | channel: Arc>, 394 | } 395 | 396 | pub struct Receiver { 397 | channel: Arc>, 398 | } 399 | 400 | struct Channel { // 不再 `pub` 401 | message: UnsafeCell>, 402 | ready: AtomicBool, 403 | } 404 | 405 | unsafe impl Sync for Channel where T: Send {} 406 | ``` 407 | 408 | 就像之前一样,我们在 T 是 Send 的情况下为 `Channel` 实现了 `Sync`,以允许它跨线程使用。 409 | 410 | 注意,我们不再像我们之前 channel 实现中的那样,需要 `in_use` 原子布尔值。它仅通过 send 来检查它有没有被调用超过一次,现在通过类型系统静态地保证。 411 | 412 | channel 函数去创建一个 channel 和一对发送者和接收者,它与我们之前的 `Channel::new` 函数类似,除了将 Channel 包装在 Arc 中,也将该 Arc 和其克隆包装在 Sender 和 Receiver 类型中: 413 | 414 | ```rust 415 | pub fn channel() -> (Sender, Receiver) { 416 | let a = Arc::new(Channel { 417 | message: UnsafeCell::new(MaybeUninit::uninit()), 418 | ready: AtomicBool::new(false), 419 | }); 420 | (Sender { channel: a.clone() }, Receiver { channel: a }) 421 | } 422 | ``` 423 | 424 | `send`、`is_ready` 和 `receive` 方法与我们之前实现的方法基本相同,但有一些区别: 425 | 426 | * 它们现在被移动到它们各自的类型中,因此只有(单个)发送者可以发送,并且只有(单个)接收者可以接收。 427 | * 发送和接收现在通过值而不是引用来接收 `self`,以确保它们每个只能被调用一次。 428 | * 发送不再 panic,因为它的先决条件(只被调用一次)现在被静态保证。 429 | 430 | 所以,他们现在看起来像这样: 431 | 432 | ```rust 433 | impl Sender { 434 | /// 从不会 panic :) 435 | pub fn send(self, message: T) { 436 | unsafe { (*self.channel.message.get()).write(message) }; 437 | self.channel.ready.store(true, Release); 438 | } 439 | } 440 | 441 | impl Receiver { 442 | pub fn is_ready(&self) -> bool { 443 | self.channel.ready.load(Relaxed) 444 | } 445 | 446 | pub fn receive(self) -> T { 447 | if !self.channel.ready.swap(false, Acquire) { 448 | panic!("no message available!"); 449 | } 450 | unsafe { (*self.channel.message.get()).assume_init_read() } 451 | } 452 | } 453 | ``` 454 | 455 | receive 函数仍然可以 panic,因为用户可能仍然会在 `is_ready()` 返回 `true` 之前调用它。它仍然使用 `swap` 将 ready 标识设置回 false(而不仅仅是 load 操作),以便 Channel 的 Drop 实现知道是否有需要删除的未读消息。 456 | 457 | 该 Drop 实现与我们之前实现的完全相同: 458 | 459 | ```rust 460 | impl Drop for Channel { 461 | fn drop(&mut self) { 462 | if *self.ready.get_mut() { 463 | unsafe { self.message.get_mut().assume_init_drop() } 464 | } 465 | } 466 | } 467 | ``` 468 | 469 | 当 `Sender` 或者 `Receiver` 被丢弃时,`Arc>` 的 Drop 实现将递减对共享内存分配的引用计数。当丢弃到第二个时,计数达到 0,并且 `Channel` 自身被丢弃。这将调用我们上面的 Drop 实现,如果已发送但未收到消息,我们将丢弃该消息。 470 | 471 | 让我们尝试它: 472 | 473 | ```rust 474 | fn main() { 475 | thread::scope(|s| { 476 | let (sender, receiver) = channel(); 477 | let t = thread::current(); 478 | s.spawn(move || { 479 | sender.send("hello world!"); 480 | t.unpark(); 481 | }); 482 | while !receiver.is_ready() { 483 | thread::park(); 484 | } 485 | assert_eq!(receiver.receive(), "hello world!"); 486 | }); 487 | } 488 | ``` 489 | 490 | 有一点不方便的是,我们仍然得手动地使用线程阻塞去等待一个消息,但是我们稍后将处理这个问题。 491 | 492 | 目前,我们的目标是在编译时使至少一种形式的滥用变得不可能。与过去不同,试图发送两次不会导致程序 Panic,相反,根本不会导致有效的程序。如果我们向上述工作程序增加另一个 send 调用,编译器现在捕捉问题并可能告知我们错误信息: 493 | 494 | ```txt 495 | error[E0382]: use of moved value: `sender` 496 | --> src/main.rs 497 | | 498 | | sender.send("hello world!"); 499 | | -------------------- 500 | | `sender` moved due to this method call 501 | | 502 | | sender.send("second message"); 503 | | ^^^^^^ value used here after move 504 | | 505 | note: this function takes ownership of the receiver `self`, which moves `sender` 506 | --> src/lib.rs 507 | | 508 | | pub fn send(self, message: T) { 509 | | ^^^^ 510 | = note: move occurs because `sender` has type `Sender<&str>`, 511 | which does not implement the `Copy` trait 512 | ``` 513 | 514 | 根据情况,设计一个在编译时捕捉错误的接口可能非常棘手。如果这种情况确实适合这样的接口,它不仅可以为用户带来更多的便利,还可以**减少**运行时检查的数量,因为这些检查在静态上已经得到保证。例如,我们不再需要 `in_use` 标识,并从发送者法中移除了交换和检查步骤。 515 | 516 | 不幸的是,可能会出现新的问题,这可能导致运行时开销。在这种情况下,问题是拆分所有权,我们不得不使用 Arc 并承受 Arc 的代价。 517 | 518 | 不得不在安全性、便利性、灵活性、简单性和性能之间进行权衡是不幸的,但有时是不可避免的。Rust 通常致力于在这些方面取得最佳表现,但有时为了最大化某个方面的优势,我们需要在其中做出一些妥协。 519 | 520 | ## 借用以避免内存分配 521 | 522 | (英文版本) 523 | 524 | 我们刚刚基于 Arc 的 channel 实现的设计可以非常方便的使用——代价是一些性能,因为它得内存分配。如果我们想要优化效率,我们可以通过用户对共享的 Channel 对象负责来获取一些性能。我们可以强制用户去创建一个通过可以由 Sender 和 Receiver 借用的 Channel,而不是在幕后处理 Channel 内存分配和所有权。这样,它们可以选择简单地放置 Channel 在局部变量中,从而避免内存分配的开销。 525 | 526 | 我们将也在一定程度上牺牲简洁性,因为我们现在不得不处理借用和生命周期。 527 | 528 | 因此,这三种类型现在看起来如下,Channel 再次公开,Sender 和 Receiver 借用它一段时间。 529 | 530 | ```rust 531 | pub struct Channel { 532 | message: UnsafeCell>, 533 | ready: AtomicBool, 534 | } 535 | 536 | unsafe impl Sync for Channel where T: Send {} 537 | 538 | pub struct Sender<'a, T> { 539 | channel: &'a Channel, 540 | } 541 | 542 | pub struct Receiver<'a, T> { 543 | channel: &'a Channel, 544 | } 545 | ``` 546 | 547 | 我们没有使用 `channel()` 函数来创建一对 Sender 和 Receiver,而是回到本章节使用的 `Channel::new`,这允许用户为此类对象创建局部变量。 548 | 549 | 此外,我们需要一种方法,让用户创建将借用 Channel 的 Sender 和 Receiver 对象。这将需要是一个独占借用(`&mut Channel`),以确保同一 channel 不能有多个发送者或接收者。通过同时提供 Sender 和 Receiver,我们可以将独占引用*分成*两个共享借用,这样发送者和接收者都可以引用 channel,同时防止其他任何东西接触 channel。 550 | 551 | 这导致我们实现以下内容: 552 | 553 | ```rust 554 | impl Channel { 555 | pub const fn new() -> Self { 556 | Self { 557 | message: UnsafeCell::new(MaybeUninit::uninit()), 558 | ready: AtomicBool::new(false), 559 | } 560 | } 561 | 562 | pub fn split<'a>(&'a mut self) -> (Sender<'a, T>, Receiver<'a, T>) { 563 | *self = Self::new(); 564 | (Sender { channel: self }, Receiver { channel: self }) 565 | } 566 | } 567 | ``` 568 | 569 | `split` 方法使用一个极其复杂的签名,值得好好观察。它通过一个独占引用独占地借用 `self`,但它分成了两个共享引用,包装在 Sender 和 Receiver 类型中。`'a` 生命周期清楚地表明,这两个对象借用了有限的生命周期的东西;在这种情况下,是 Channel 本身的生命周期。由于 Channel 是独占地借用,只要 Sender 或 Receiver 对象存在,调用者不能去借用或者移动它。 570 | 571 | 然而,一旦这些对象都不再存在,可变的借用就会过期,编译器会愉快地让 Channel 对象通过第二次调用 `split()` 再次被借用。尽管我们可以假设在 Sender 和 Receiver 存在时,不能再次调用 `split()`,我们不能阻止在这些对象被丢弃或者遗忘后再次调用 `split()`。我们需要确保我们不能偶然地在 channel 已经有它的 ready 标识设置的情况下创建新的 Sender 或 Receiver 对象,因为这将打包阻止未定义行为的假设。 572 | 573 | 通过在 `split()` 中用新的空 channel 覆盖 `*self`,我们确保它在创建 Sender 和 Receiver 状态时处于预期状态。这也会在旧的 `*self` 上调用 Drop 实现,它将负责丢弃之前发送但从未接收的消息。 574 | 575 | > 由于 split 的签名的生命周期来自 `self`,它可以被省略。上面片段的 `split` 签名与这个不太冗长的版本相同 576 | > 577 | > ```rust 578 | > pub fn split(&mut self) -> (Sender, Receiver) { … } 579 | > ``` 580 | > 581 | > 虽然此版本没有明确显示返回的对象借用了 self,但编译器仍然与更冗长的版本完全一样检查生命周期的正确使用情况。 582 | 583 | 其余的方法和 Drop 实现与我们基于 Arc 的实现相同,除了 Sender 和 Receiver 类型的额外 `'_` 生命周期参数。(如果你忘记了这些,编译器会建议添加它们。) 584 | 585 | 为了完全起效,以下是剩余的代码: 586 | 587 | ```rust 588 | impl Sender<'_, T> { 589 | pub fn send(self, message: T) { 590 | unsafe { (*self.channel.message.get()).write(message) }; 591 | self.channel.ready.store(true, Release); 592 | } 593 | } 594 | 595 | impl Receiver<'_, T> { 596 | pub fn is_ready(&self) -> bool { 597 | self.channel.ready.load(Relaxed) 598 | } 599 | 600 | pub fn receive(self) -> T { 601 | if !self.channel.ready.swap(false, Acquire) { 602 | panic!("no message available!"); 603 | } 604 | unsafe { (*self.channel.message.get()).assume_init_read() } 605 | } 606 | } 607 | 608 | impl Drop for Channel { 609 | fn drop(&mut self) { 610 | if *self.ready.get_mut() { 611 | unsafe { self.message.get_mut().assume_init_drop() } 612 | } 613 | } 614 | } 615 | ``` 616 | 617 | 让我们来测试它! 618 | 619 | ```rust 620 | fn main() { 621 | let mut channel = Channel::new(); 622 | thread::scope(|s| { 623 | let (sender, receiver) = channel.split(); 624 | let t = thread::current(); 625 | s.spawn(move || { 626 | sender.send("hello world!"); 627 | t.unpark(); 628 | }); 629 | while !receiver.is_ready() { 630 | thread::park(); 631 | } 632 | assert_eq!(receiver.receive(), "hello world!"); 633 | }); 634 | } 635 | ``` 636 | 637 | 与基于 Arc 的版本相比,便利性的**减少**非常小:我们只需要多一行代码来手动创建一个 Channel 对象。然而,请注意,channel 必须在作用域之前创建,以向编译器证明其存在超过 Sender 和 Receiver 的时间。 638 | 639 | 要查看编译器的借用检查器的实际操作,请尝试在各个地方添加对 `channel.split()` 的第二次调用。你将看到,在线程作用域内第二次调用它会导致错误,而在作用域之后调用它是可以接受的。即使在作用域之前调用 `split()` 也没问题,只要你在作用域开始之前停止使用返回的 Sender 和 Receiver 。 640 | 641 | ## 阻塞 642 | 643 | (英文版本) 644 | 645 | 让我们最终处理一下我们 Channel 最后留下的最大不便,阻塞接口的缺乏。我们测试一个新的 channel 变体,每次都使用线程阻塞函数。将这种模式本身整合到 channel 应该不是太难。 646 | 647 | 为了能够释放接收者,发送者需要知道去释放哪个线程。`std::thread::Thread` 类型表示线程的句柄,正是我们调用 `unpark()` 所需要的。我们将把句柄存储到 Sender 对象内的接收线程,如下所示: 648 | 649 | ```rust 650 | use std::thread::Thread; 651 | 652 | pub struct Sender<'a, T> { 653 | channel: &'a Channel, 654 | receiving_thread: Thread, // 新增! 655 | } 656 | ``` 657 | 658 | 然而,如果 Receiver 对象在线程之间发送,该句柄将引用错误的线程。Sender 将不会意识到这个,并且仍然会参考最初持有 Receiver 的线程。 659 | 660 | 我们可以通过使 Receiver 更具限制性,不再允许它在线程之间发送来处理这个问题。正如[第 1 章“线程安全:Send 和 Sync”](./1_Basic_of_Rust_Concurrency.md#线程安全send-和-sync)中所讨论的,我们可以使用特殊的 `PhantomData` 标记类型将此限制添加到我们的结构中。`PhantomData<*const ()>` 将完成这项工作,因为原始指针,如 `*const ()`,没有实现 Send: 661 | 662 | ```rust 663 | pub struct Receiver<'a, T> { 664 | channel: &'a Channel, 665 | _no_send: PhantomData<*const ()>, // 新增! 666 | } 667 | ``` 668 | 669 | 接下来,我们必须修改 `Channel::split` 方法来填充新字段,例如: 670 | 671 | ```rust 672 | pub fn split<'a>(&'a mut self) -> (Sender<'a, T>, Receiver<'a, T>) { 673 | *self = Self::new(); 674 | ( 675 | Sender { 676 | channel: self, 677 | receiving_thread: thread::current(), // 新增! 678 | }, 679 | Receiver { 680 | channel: self, 681 | _no_send: PhantomData, // 新增! 682 | } 683 | ) 684 | } 685 | ``` 686 | 687 | 我们使用当前线程的句柄来填充 `receiving_thread` 字段,因为我们返回的 Receiver 对象将保留在当前线程上。 688 | 689 | 正如以下展示的,`send` 方法并不做改变。我们仅在 `receiving_thread` 字段上调用 `unpark()` 去唤醒接收者,以防止它正在等待: 690 | 691 | ```rust 692 | impl Sender<'_, T> { 693 | pub fn send(self, message: T) { 694 | unsafe { (*self.channel.message.get()).write(message) }; 695 | self.channel.ready.store(true, Release); 696 | self.receiving_thread.unpark(); // 新增! 697 | } 698 | } 699 | ``` 700 | 701 | receive 函数发生的变化稍大。如果它仍然没有消息,新版本不会 panic,而是使用 `thread::park()` 等待消息并再次尝试,并根据需要多次重试。 702 | 703 | ```rust 704 | impl Receiver<'_, T> { 705 | pub fn receive(self) -> T { 706 | while !self.channel.ready.swap(false, Acquire) { 707 | thread::park(); 708 | } 709 | unsafe { (*self.channel.message.get()).assume_init_read() } 710 | } 711 | } 712 | ``` 713 | 714 | > 请记住,`thread::park()` 可能会虚假返回。(或者因为除了我们的 send 方法以外的其它原因调用了 `unpark()`。)这意味着我们不能假设 `park()` 返回时已经设置了 ready 标识。因此,我们需要使用一个循环,在唤醒后再次检查 ready 标识。 715 | 716 | `Channel` 结构体、它的 Sync 实现、它的 new 函数以及它的 Drop 实现保持不变。 717 | 718 | 让我们尝试它! 719 | 720 | ```rust 721 | fn main() { 722 | let mut channel = Channel::new(); 723 | thread::scope(|s| { 724 | let (sender, receiver) = channel.split(); 725 | s.spawn(move || { 726 | sender.send("hello world!"); 727 | }); 728 | assert_eq!(receiver.receive(), "hello world!"); 729 | }); 730 | } 731 | ``` 732 | 733 | 显然,这个 Channel 比上一个 Channel 更方便使用,至少在这个简单的测试程序中是这样。我们不得不牺牲一些灵活性来创造这种便利性:只有调用 `split()` 的线程才能调用 `receive()`。如果你交换 send 和 receive 行,此程序将不再编译。根据用例,这可能完全没问题、有用或非常不方便。 734 | 735 | 确实,有许多方法解决这个问题,其中有很多会增加一些额外的复杂度并影响一些性能。总的来说,我们可以继续探索的变种和权衡是无穷无尽的。 736 | 737 | 我们很容易花费大量的时间实现 20 个一次性 channel 不同的变体,每个变体都具有不同的属性,适用于每个可以想象到的用例甚至更多。尽管这听起来很有趣,但是我们应该避免陷入这个歧途,并在事情失控之前结束本章。 738 | 739 | ## 总结 740 | 741 | (英文版本) 742 | 743 | * *channel* 用于在线程之间发送*消息*。 744 | * 一个简单、灵活但可能效率低下的 channel,只需一个 `Mutex` 和 `Condvar` 就很容易实现。 745 | * *一次性*(one-shot)channel 是一个被设计仅发送一次信息的 channel。 746 | * `MaybeUninit` 类型可用于表示可能尚未初始化的 `T`。其接口大多不安全,使用户负责跟踪其是否已初始化,不要复制非 `Copy` 数据,并在必要时删除其内容。 747 | * 不丢弃对象(也称为泄漏或者遗忘)是安全的,但如果没有充分理由而这样做,会被视为不良的做法。 748 | * panic 是创建安全接口的重要工具。 749 | * 按值获取一个非 Copy 对象可以用于阻止某个操作被重复执行。 750 | * 独占借用和拆分借用是确保正确性的强大工具。 751 | * 我们可以确保对象的类型不实现 `Send`,确保它在同一个线程,这可以通过 `PhantomData` 标记实现。 752 | * 每个设计和实施决定都涉及权衡,最好在考虑特定用例的情况下做出。 753 | * 在没有用例的情况下设计一些东西可能是有趣的和有教育意义的,但是这可能是一个无止境的任务。 754 | 755 |

756 | 下一篇,第六章:构建我们自己的“Arc” 757 |

758 | -------------------------------------------------------------------------------- /6_Building_Our_Own_Arc.md: -------------------------------------------------------------------------------- 1 | # 第六章:构建我们自己的“Arc” 2 | 3 | (英文版本) 4 | 5 | 6 | 在[第一章“引用计数”](./1_Basic_of_Rust_Concurrency.md#引用计数)中,我们了解了 `std::sync::Arc` 类型允许通过引用计数共享所有权。`Arc::new` 函数创建一个新的内存分配,就像 `Box::new`。然而,与 Box 不同的是,克隆 Arc 将共享原始的内存分配,而不是创建一个新的。只有当 Arc 和所有其他的克隆被丢弃,共享的内存分配才会被丢弃。 7 | 8 | 这种类型的实现所涉及的内存排序可能是非常有趣的。在本章中,我们将通过实现我们自己的 `Arc` 将更多理论付诸实践。我们将开始一个基础的版本,然后将其扩展到支持循环结构的 *weak 指针*,并且最终将其优化为一个与标准库差不多的实现结束本章。 9 | 10 | ## 基础的引用计数 11 | 12 | (英文版本) 13 | 14 | 我们的第一个版本将使用单个 `AtomicUsize` 去计数 Arc 对象共享分配的数量。让我们开始使用一个持有计数器和 T 对象的结构体: 15 | 16 | ```rust 17 | struct ArcData { 18 | ref_count: AtomicUsize, 19 | data: T, 20 | } 21 | ``` 22 | 23 | 注意,该结构体不是公共的。它是我们 Arc 实现的内部实现细节。 24 | 25 | 接下来是 `Arc` 结构体本身,它实际上仅是一个指向(共享的)`ArcData` 的指针。 26 | 27 | 使用 `Box>` 作为包装器,并使用标准的 Box 来处理 `ArcData` 的内存分配可能很诱人。然而,Box 表示独占所有权,并不是共享所有权。我们不能使用引用,因为我们不仅要借用其他所有权的数据,并且它的生命周期(“直到此 Arc 的最后一个克隆被丢弃”)无法直接表示为 Rust 的生命周期。 28 | 29 | 相反,我们将不得不使用指针,并手动处理内存分配以及所有权的概念。我们将使用 `std::ptr::NonNull`,而不是 `*mut T` 或 `*const T`,它表示一个永远不会为空的指向 T 的指针。这样,使用 None 的空指针表示 `Option>` 与 `Arc` 的大小相同。 30 | 31 | ```rust 32 | use std::ptr::NonNull; 33 | 34 | pub struct Arc { 35 | ptr: NonNull>, 36 | } 37 | ``` 38 | 39 | 使用一个引用或者 Box,编译器会自动地理解它会为哪个 T 实现 Send 和 Sync。然而,当使用原始指针或者 `NonNull`,除非我们明确告知,否则它会保守地认为它永远不会 Send 或 Sync。 40 | 41 | 发送 `Arc` 跨线程会导致 T 对象被共享,这时要求 T 实现 Sync。类似地,将 `Arc` 跨线程发送可能导致另一个线程丢弃该对象,从而将它转移到其他线程,这里要求 T 实现 Send。换句话说,如果 T 既是 Send 又是 Sync,那么 `Arc` 应该是 Send。对于 Sync 来说也是完全相同的,因为一个共享的 `&Arc` 可以克隆为一个新的 `Arc`。 42 | 43 | ```rust 44 | unsafe impl Send for Arc {} 45 | unsafe impl Sync for Arc {} 46 | ``` 47 | 48 | 对于 `Arc::new`,我们必须使用引用计数为 1 的 `ArcData` 创建一个新的内存分配。我们将使用 `Box::new` 创建新的内存分配,使用 `Box::leak` 放弃我们对此内存分配的独占所有权,以及使用 `NonNull::from` 将其转换为指针: 49 | 50 | ```rust 51 | impl Arc { 52 | pub fn new(data: T) -> Arc { 53 | Arc { 54 | ptr: NonNull::from(Box::leak(Box::new(ArcData { 55 | ref_count: AtomicUsize::new(1), 56 | data, 57 | }))), 58 | } 59 | } 60 | 61 | // … 62 | } 63 | ``` 64 | 65 | 我们知道只要 Arc 对象存在,指针将总是指向一个有效的 `ArcData`。然而,这不是编译器知道的或为我们检查的内容,所以通过指针访问 `ArcData` 需要不安全的代码。我们将添加一个私有的辅助函数去从 Arc 获取`ArcData`,因为这是我们将执行多次的操作: 66 | 67 | ```rust 68 | fn data(&self) -> &ArcData { 69 | unsafe { self.ptr.as_ref() } 70 | } 71 | ``` 72 | 73 | 使用它,我们现在可以实现 `Deref` trait 以使得我们的 `Arc` 能够像 T 的引用一样透明地操作: 74 | 75 | ```rust 76 | impl Deref for Arc { 77 | type Target = T; 78 | 79 | fn deref(&self) -> &T { 80 | &self.data().data 81 | } 82 | } 83 | ``` 84 | 85 | 注意,我们并没有实现 `DerefMut`。因为 `Arc` 表示共享所有权,我们不能无条件地提供 `&mut T`。 86 | 87 | 接下来:实现 Clone。在**增加**引用计数后,克隆的 Arc 将使用相同的指针: 88 | 89 | ```rust 90 | impl Clone for Arc { 91 | fn clone(&self) -> Self { 92 | // TODO: Handle overflows. 93 | self.data().ref_count.fetch_add(1, Relaxed); 94 | Arc { 95 | ptr: self.ptr, 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | 我们可以使用 Relaxed 内存排序去递增引用计数,因为没有对其他变量的操作在此原子操作之前或者之后严格地发生。在此操作之前,我们已经可以访问 Arc 包含的 T(通过原始的 Arc),在此操作之后,访问仍然没有改变(但现在至少有两个相同 Arc 对象可以访问数据)。 102 | 103 | 一个 Arc 需要被克隆多次才有可能导致引用计数溢出,但是在循环中运行 `std::men::forget()` 可以实现这一点。我们可以使用在[第二章的“示例:ID 分配”](./2_Atomics.md#示例id-分配)和[“示例:没有溢出的 ID 分配”](./2_Atomics.md#示例没有溢出的-id-分配)中讨论的任意技术来处理这个问题。 104 | 105 | 为了在正常(非溢出)情况下保持尽可能高效,我们将保留原始的 `fetch_add`,并在接近溢出时简单地中止整个过程: 106 | 107 | ```rust 108 | if self.data().ref_count.fetch_add(1, Relaxed) > usize::MAX / 2 { 109 | std::process::abort(); 110 | } 111 | ``` 112 | 113 | > 并不会立刻中止进程,在一段时间内,另一个线程也可以调用 `Arc::clone`,进一步**增加**引用计数器。因此,仅仅检查 `usize::MAX - 1` 将是不够的。然而,使用 `usize::MAX / 2` 限制工作是好的:假设每个线程在内存中至少需要几字节的空间,那么并发存在 `usize::MAX / 2` 数量的线程是不可能的。 114 | 115 | 就像我们在克隆时递增计数器一样,我们在丢弃 Arc 时需要递减计数器。线程看到计数器从 1 到 0,这意味着该线程丢弃了最后一个 `Arc`,并负责丢弃和释放 `ArcData`。 116 | 117 | 我们将使用 `Box::from_raw` 去重新获得内存的独占所有权,然后立即使用 `drop()` 将其丢弃: 118 | 119 | ```rust 120 | impl Drop for Arc { 121 | fn drop(&mut self) { 122 | // TODO:内存排序 123 | if self.data().ref_count.fetch_sub(1, …) == 1 { 124 | unsafe { 125 | drop(Box::from_raw(self.ptr.as_ptr())); 126 | } 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | 133 | 对于这个操作,我们不能使用 Relaxed 排序,因为我们需要确保当我们丢弃它时,没有任何东西仍然在访问数据。换句话说,每个之前的 Arc 丢弃操作都必须发生在最终丢弃之前。因此,最后的 `fetch_sub` 必须与之前的 `fetch_sub` 操作建立一个 happens-before 关系,我们可以使用 release 和 acquire 排序来实现这一点:例如,从 2 递减到 1 可以有效地“释放”数据,而从 1 递减到 0 则“获取”了对它的所有权。 134 | 135 | 我们可以使用 `AcqRel` 内存排序来覆盖这两种情况,但只有最后一个递减到 0 才需要 `Acquire`,而其他情况只需要 `Release`。为了提高效率,我们将在 `Release` 用于 `fetch_sub` 操作,并且仅在必要时使用单独的 `Acquire` 屏障: 136 | 137 | ```rust 138 | if self.data().ref_count.fetch_sub(1, Release) == 1 { 139 | fence(Acquire); 140 | unsafe { 141 | drop(Box::from_raw(self.ptr.as_ptr())); 142 | } 143 | } 144 | ``` 145 | 146 | ### 测试它 147 | 148 | (英文版本) 149 | 150 | 为了测试我们的 Arc 是否按预期运行,我们可以编写一个单元测试,创建一个包含特殊对象的 `Arc`,让我们知道何时它被丢弃时: 151 | 152 | ```rust 153 | #[test] 154 | fn test() { 155 | static NUM_DROPS: AtomicUsize = AtomicUsize::new(0); 156 | 157 | struct DetectDrop; 158 | 159 | impl Drop for DetectDrop { 160 | fn drop(&mut self) { 161 | NUM_DROPS.fetch_add(1, Relaxed); 162 | } 163 | } 164 | 165 | // 创建两个 Arc,共享一个对象,包含一个字符串 166 | // 和一个 DetectDrop,以当它被丢弃时去检测。 167 | let x = Arc::new(("hello", DetectDrop)); 168 | let y = x.clone(); 169 | 170 | // 发送 x 到另一个线程,并在那里使用它。 171 | let t = std::thread::spawn(move || { 172 | assert_eq!(x.0, "hello"); 173 | }); 174 | 175 | // 这是并行的,y 应该仍然在这里可用。 176 | assert_eq!(y.0, "hello"); 177 | 178 | // 等待线程完成。 179 | t.join().unwrap(); 180 | 181 | // Arc,x 现在应该被丢弃。 182 | // 我们仍然有 y,因此对象仍然还没有被丢弃。 183 | assert_eq!(NUM_DROPS.load(Relaxed), 0); 184 | 185 | // 丢弃剩余的 `Arc`。 186 | drop(y); 187 | 188 | // 现在,`y` 也被丢弃, 189 | // 对象应该也被丢弃。 190 | assert_eq!(NUM_DROPS.load(Relaxed), 1); 191 | } 192 | ``` 193 | 194 | 编译并运行良好,因此我们的 Arc 似乎按预期运行!虽然这令人兴奋,但并不能证明实现是完全正确的。建议使用涉及多个线程的长压力测试,以获得更多的信心。 195 | 196 |
197 |

Miri

198 |

使用 Miri 运行测试是非常有用的。Miri 是一个实验性,但非常有用并且强力的工具,用于检查各种未定义形式的不安全代码。

199 | 200 |

Miri 是 Rust 编译器中间级别中间表示的解释器。这意味着它不会将你的代码编译成本机处理器指令,而是通过当像类型和生命周期是仍然可用时进行解释。因此,Miri 运行程序的速度比正常运行速度慢很多,但能够检测许多导致未定义行为的错误。

201 | 202 |

它包括检测数据竞争的实验性支持,这允许它检测内存排序问题。

203 | 204 |

有关更多使用 Miri 的细节和指导,请参见它的 GitHub 页面

205 |
206 | 207 | ### 可变性 208 | 209 | (英文版本) 210 | 211 | 正如之前提及的,我们不能为我们的 Arc 实现 DerefMut。我们不能无条件地承诺对数据的独占访问(`&mut T`),因为它能够通过其他 Arc 对象访问。 212 | 213 | 然而,我们可以有条件地允许独占访问。我们可以创建一个方法,如果引用计数为 1,则提供 `&mut T`,这证明没有其他 Arc 对象可以用来访问相同的数据。 214 | 215 | 216 | 该函数我们将称它为 `get_mut`,它必须接受一个 `&mut Self` 以确保没有其他的相同的东西使用 Arc 获取 T。如果这个 Arc 仍然可以共享,知道只有一个 Arc 对象是没有意义的。 217 | 218 | 219 | 我们需要使用 acquire 内存排序去确保之前拥有 Arc 克隆的线程不再访问数据。我们需要与导致引用计数为 1 的每个单独的 `drop` 建立一个 happens-before 关系。 220 | 221 | 这仅在引用计数实际为 1 时才重要:如果引用计数高,我们将不再提供一个 `&mut T`,并且内存排序是无关紧要。因此,我们可以使用 relaxed load 操作,随后跟条件行的 acquire 屏障,如下所示: 222 | 223 | ```rust 224 | pub fn get_mut(arc: &mut Self) -> Option<&mut T> { 225 | if arc.data().ref_count.load(Relaxed) == 1 { 226 | fence(Acquire); 227 | // 安全性:没有任何其他东西可以访问 data,因为 228 | // 只有一个 Arc,我们拥有独占访问的权限。 229 | unsafe { Some(&mut arc.ptr.as_mut().data) } 230 | } else { 231 | None 232 | } 233 | } 234 | ``` 235 | 236 | 该函数并不接收 `self` 参数,而是接受一个常规的参数(名称 `arc`)。这意味着,它仅可以 `Arc::get_mut(&mut a)` 这样的方式调用,而不以 `a.get_mut()` 方式调用。对于实现了 `Deref` 的类型来说,这是可取的,以避免与底层类型 `T` 上的同名方法产生歧义。 237 | 238 | 返回的可以引用会隐式地从参数重借用生命周期,这意味着只要返回 `&mut T` 仍然存在,原始的 Arc 就不能被其他代码使用,从而允许安全的可变性操作。 239 | 240 | 当 `&mut T` 的生命周期过期后,Arc 可以在此被使用以及与其他线程共享。也许有人可能会想知道,在之后访问数据的线程是否需要关注内存排序。然而,这是用于与其他线程共享 Arc(或着新克隆)的机制负责的。(例如 mutex、channel 或者产生的新线程。) 241 | 242 | ## Weak 指针 243 | 244 | (英文版本) 245 | 246 | 当表示在内存中多个对象组成的结构时,引用计数非常有用。例如,在树结构中的每个节点可以包含对其子节点的 Arc 引用。这样,当我们丢弃一个节点时,不再使用的孩子节点也会被(递归地)丢弃。 247 | 248 | 249 | 然而,对于*循环结构*来说,这会失效。如果一个子节点也包含对它父节点的 Arc 引用,那么当所有 Arc 引用都不存在时,两者都不会被丢弃,因为始终存至少有一个 Arc 引用仍然指向它们。 250 | 251 | 标准库的 Arc 提供了解决这个问题的办法:`Weak`。`Weak`(也被称为 *weak 指针*),行为有点像 `Arc`,但是并不会阻止对象被丢弃。T 可以在多个 `Arc` 和 `Weak` 对象之间共享,但是当所有 `Arc` 对象都消失时,不管是否还有 `Weak` 对象,T 都会被丢弃。 252 | 253 | 这意味着 `Weak` 可以没有 T 而存在,因此无法像 Arc 那样无条件地提供 `&T`。然而,为了获取给定 `Weak` 中的 T,可以通过 `Arc` 的 `upgrade()` 方法来升级。这个方法返回一个 `Option>`,如果 T 已经被丢弃,则返回 None。 254 | 255 | 在基于 Arc 的结构中,可以使用 Weak 打破循环引用。例如,在树结构中的子节点使用 Weak,而不是使用 Arc 来引用它们的父节点。然后,尽管子节点存在,也不会阻止父节点被丢弃。 256 | 257 | 让我们来实现这个功能。 258 | 259 | 与之前一样,当 Arc 对象的数量到达 0 时,我们可以丢弃包含 T 的对象。然而,我们仍然不能丢弃和释放 ArcData,因为可能仍然有 weak 指针指向它。只有当最后一个 Weak 指针也不存在时,我们才能丢弃和释放 ArcData。 260 | 261 | 因此,我们将使用两个计数器:一个计算“引用 T 对象的数量”,另一个计算“引用 `ArcData` 对象的数量”。换句话说,第一个计数器与之前相同:它计算 Arc 对象的数量,而第二个计数器计算 Arc 和 Weak 对象的数量。 262 | 263 | 我们还需要一种在 `ArcData` 被 weak 指针使用时,允许我们丢弃包含的对象(T)的机制。我们将使用 `Option`,这样当数据被丢弃时可以使用 None,并将其包装在 UnsafeCell 中进行内部可变性([在第一章“内部可变性”中](./1_Basic_of_Rust_Concurrency.md#内部可变性)),以允许在 `ArcData` 不是独占所有权时发生这种情况: 264 | 265 | ```rust 266 | struct ArcData { 267 | /// `Arc` 的数量 268 | data_ref_count: AtomicUsize, 269 | /// `Arc` 和 `Weak` 总共的数量。 270 | alloc_ref_count: AtomicUsize, 271 | /// 持有的数据。如果仅剩下 weak 指针,则是 `None`。 272 | data: UnsafeCell>, 273 | } 274 | ``` 275 | 276 | 如果我们认为 `Weak` 是保持 `ArcData` 存活的对象,那么将 `Arc` 实现为包含 `Weak` 的结构体可能是有意义的,因为 `Arc` 需要做相同的事情,而且还有更多的功能。 277 | 278 | ```rust 279 | pub struct Arc { 280 | weak: Weak, 281 | } 282 | 283 | pub struct Weak { 284 | ptr: NonNull>, 285 | } 286 | 287 | unsafe impl Send for Weak {} 288 | unsafe impl Sync for Weak {} 289 | ``` 290 | 291 | 新函数与之前的基本相同,除了它现在有两个计数器可以同时初始化: 292 | 293 | ```rust 294 | impl Arc { 295 | pub fn new(data: T) -> Arc { 296 | Arc { 297 | weak: Weak { 298 | ptr: NonNull::from(Box::leak(Box::new(ArcData { 299 | alloc_ref_count: AtomicUsize::new(1), 300 | data_ref_count: AtomicUsize::new(1), 301 | data: UnsafeCell::new(Some(data)), 302 | }))), 303 | }, 304 | } 305 | } 306 | 307 | //… 308 | } 309 | ``` 310 | 311 | 就像之前一样,我们假设 ptr 字段总是指向有效的 `ArcData`。这一次,我们将在 `Weak` 上将该假设编码为私有 `data()` 辅助方法: 312 | 313 | ```rust 314 | impl Weak { 315 | fn data(&self) -> &ArcData { 316 | unsafe { self.ptr.as_ref() } 317 | } 318 | 319 | // … 320 | } 321 | ``` 322 | 323 | 在 `Arc` 的 Deref 实现中,我们现在不得不使用 `UnsafeCell::get()` 拉取得到 cell 内容的指针,并使用不安全的代码去承诺它此时可以共享。我们也需要 `as_ref().unwrap()` 去获取 `Option` 引用。我们不必担心引发 panic,因为只有在没有 Arc 对象时 Option 才会为 None。 324 | 325 | ```rust 326 | impl Deref for Arc { 327 | type Target = T; 328 | 329 | fn deref(&self) -> &T { 330 | let ptr = self.weak.data().data.get(); 331 | // 安全性:由于 Arc 包装 data, 332 | // data 存在并可以共享。 333 | unsafe { (*ptr).as_ref().unwrap() } 334 | } 335 | } 336 | ``` 337 | 338 | `Weak` 的克隆实现非常简单;它与我们之前 `Arc` 的克隆实现几乎相同: 339 | 340 | ```rust 341 | impl Clone for Weak { 342 | fn clone(&self) -> Self { 343 | if self.data().alloc_ref_count.fetch_add(1, Relaxed) > usize::MAX / 2 { 344 | std::process::abort(); 345 | } 346 | Weak { ptr: self.ptr } 347 | } 348 | } 349 | ``` 350 | 351 | 在我们新 `Arc` 的克隆实现中,我们需要同时递增两个计数器。我们将简单地使用 `self.weak.clone()` 为第一个计数器重用上面的代码,因此我们只需要手动递增第二个计数器: 352 | 353 | ```rust 354 | impl Clone for Arc { 355 | fn clone(&self) -> Self { 356 | let weak = self.weak.clone(); 357 | if weak.data().data_ref_count.fetch_add(1, Relaxed) > usize::MAX / 2 { 358 | std::process::abort(); 359 | } 360 | Arc { weak } 361 | } 362 | } 363 | ``` 364 | 365 | 当计数器从 1 到 0 时,丢弃 Weak 应该递减它的计数,以及丢弃和释放 ArcData。这与我们之前 Arc 的 Drop 实现相同。 366 | 367 | ```rust 368 | impl Drop for Weak { 369 | fn drop(&mut self) { 370 | if self.data().alloc_ref_count.fetch_sub(1, Release) == 1 { 371 | fence(Acquire); 372 | unsafe { 373 | drop(Box::from_raw(self.ptr.as_ptr())); 374 | } 375 | } 376 | } 377 | } 378 | ``` 379 | 380 | 丢弃 Arc 应该同时递减两个计数器。注意,其中一个计数器已经被自动地处理,因为每个 Arc 都包含一个 Weak,因此删除 Arc 也会删除一个 Weak。我们仅需要处理另一个计数器: 381 | 382 | ```rust 383 | impl Drop for Arc { 384 | fn drop(&mut self) { 385 | if self.weak.data().data_ref_count.fetch_sub(1, Release) == 1 { 386 | fence(Acquire); 387 | let ptr = self.weak.data().data.get(); 388 | // 安全性:data 引用计数是 0, 389 | // 因此没有任何东西可以访问它。 390 | unsafe { 391 | (*ptr) = None; 392 | } 393 | } 394 | } 395 | } 396 | ``` 397 | 398 | 在 Rust 中丢弃一个对象将首先运行它的 `Drop::drop` 函数(如果它实现了 `Drop`),然后递归地逐个地丢弃它的所有字段。 399 | 400 | `get_mut` 方法中的检查基本上保持不变,除了现在需要考虑 weak 指针。看起来似乎可以在检查独占性时忽略 weak 指针,但是 `Weak` 可以随时升级为 `Arc`。因此,在给出 `&mut T` 之前,`get_mut` 必须检查是否还有其他 `Arc` 或者 `Weak` 指针: 401 | 402 | ```rust 403 | impl Arc { 404 | // … 405 | 406 | pub fn get_mut(arc: &mut Self) -> Option<&mut T> { 407 | if arc.weak.data().alloc_ref_count.load(Relaxed) == 1 { 408 | fence(Acquire); 409 | // 安全性:没有任何东西可以访问 data,因为 410 | // 仅有一个 Arc,并且我们拥有独占访问权限, 411 | // 也没有 Weak 指针 412 | let arcdata = unsafe { arc.weak.ptr.as_mut() }; 413 | let option = arcdata.data.get_mut(); 414 | // 我们知道 data 是仍然可获得的,因为我们 415 | // 有一个 Arc 去包裹它,因此不会 panic。 416 | let data = option.as_mut().unwrap(); 417 | Some(data) 418 | } else { 419 | None 420 | } 421 | } 422 | 423 | // … 424 | } 425 | ``` 426 | 427 | 接下来:是升级 Weak 指针。当数据仍然存在时,才能升级 Weak 到 Arc。如果仅剩下 Weak 指针,则没有数据通过 Arc 共享了。因此,我们需要递增 Arc 的计数器,但只能在计数器不为 0 时才能这样做。我们将使用「比较并交换」循环([第二章的“比较并交换操作”](./2_Atomics.md#比较并交换操作))来做这些。 428 | 429 | 与之前一样,对于递增引用计数,relaxed 内存排序是好的。在这个原子操作之前或之后,没有其他变量的操作需要严格执行。 430 | 431 | ```rust 432 | impl Weak { 433 | //… 434 | 435 | pub fn upgrade(&self) -> Option> { 436 | let mut n = self.data().data_ref_count.load(Relaxed); 437 | loop { 438 | if n == 0 { 439 | return None; 440 | } 441 | assert!(n <= usize::MAX / 2); 442 | if let Err(e) = 443 | self.data() 444 | .data_ref_count 445 | .compare_exchange_weak(n, n + 1, Relaxed, Relaxed) 446 | { 447 | n = e; 448 | continue; 449 | } 450 | return Some(Arc { weak: self.clone() }); 451 | } 452 | } 453 | } 454 | ``` 455 | 456 | 相反,从 `Arc` 获得 `Weak` 要简单得多: 457 | 458 | ```rust 459 | impl Arc { 460 | // … 461 | 462 | pub fn downgrade(arc: &Self) -> Weak { 463 | arc.weak.clone() 464 | } 465 | } 466 | ``` 467 | 468 | ### 测试它2 469 | 470 | (英文版本) 471 | 472 | 为了快速测试我们创建的内容,我们将修改之前的单元测试,以使用 weak 指针,并验证它们是否可以在预期的情况下升级: 473 | 474 | ```rust 475 | #[test] 476 | fn test() { 477 | static NUM_DROPS: AtomicUsize = AtomicUsize::new(0); 478 | 479 | struct DetectDrop; 480 | 481 | impl Drop for DetectDrop { 482 | fn drop(&mut self) { 483 | NUM_DROPS.fetch_add(1, Relaxed); 484 | } 485 | } 486 | 487 | // 创建一个 Arc,同时也创建两个 weak 指针。 488 | let x = Arc::new(("hello", DetectDrop)); 489 | let y = Arc::downgrade(&x); 490 | let z = Arc::downgrade(&x); 491 | 492 | let t = std::thread::spawn(move || { 493 | // 此刻,Weak 指针应该被升级。 494 | let y = y.upgrade().unwrap(); 495 | assert_eq!(y.0, "hello"); 496 | }); 497 | assert_eq!(x.0, "hello"); 498 | t.join().unwrap(); 499 | 500 | // data 仍然不应该被丢弃, 501 | // 并且 weak 指针应该被升级。 502 | assert_eq!(NUM_DROPS.load(Relaxed), 0); 503 | assert!(z.upgrade().is_some()); 504 | 505 | drop(x); 506 | 507 | // 现在,data 已经被丢弃,并且 508 | // weak 指针应该不再被升级。 509 | assert_eq!(NUM_DROPS.load(Relaxed), 1); 510 | assert!(z.upgrade().is_none()); 511 | } 512 | ``` 513 | 514 | 这也毫无问题地编译和运行,这给我们留下了一个非常可用的手工 Arc 实现。 515 | 516 | ### 优化 517 | 518 | (英文版本) 519 | 520 | 521 | 虽然 weak 指针是可用的,但 Arc 类型通常用于没有任何 weak 的情况下。我们上次实现的缺点是,克隆和丢弃 Arc 现在都需要两个原子操作,因为它们不得不递增或递减两个计数器。这使得 Arc 用于丢弃 weak 指针的开销增大,即使它们没有使用 weak 指针。 522 | 523 | 似乎解决的方案是分别计算 `Arc` 和 `Weak` 指针的计数,但那样我们将无法原子地检测这两个计数器是否为 0。为了理解这个问题,想象我们有一个线程执行以下令人恼火的函数: 524 | 525 | ```rust 526 | fn annoying(mut arc: Arc) { 527 | loop { 528 | let weak = Arc::downgrade(&arc); 529 | drop(arc); 530 | println!("I have no Arc!"); // 1 531 | arc = weak.upgrade().unwrap(); 532 | drop(weak); 533 | println!("I have no Weak!"); // 2 534 | } 535 | } 536 | ``` 537 | 538 | 该线程不断降级和升级一个 Arc,以至于它反复地循环未持有 Arc(1)和未持有 Weak(2)的片刻。如果我们同时检查两个计数器,查看是否还有线程仍然使用的内存,如果我们不幸地在它的第一个输出语句(1)期间检查 `Arc` 计数,但在第二个输出语句(2)期间检查 `Weak` 计数器,该线程能够隐藏它的存在。 539 | 540 | 在我们上次实现中,我们通过将每个 `Arc` 也计为 `Weak` 来解决了这个问题。一种更微妙的解决是将所有的 Arc 指针合并为一个单独的 Weak 指针进行计数。这样,只要周围仍然有一个 Arc 对象,weak 指针计数器(`alloc_ref_count`)将从不会到达 0,就像在我们上次实现中一样,但是克隆的 Arc 不需要触及该计数器。只有当最后一个 Arc 被丢弃时,weak 指针计数才会递减。 541 | 542 | 让我们尝试它。 543 | 544 | 这次,我们不能简单地将 `Arc` 实现为对 `Weak` 的包装,所以两者都将包装一个非空指针到内存分配中: 545 | 546 | ```rust 547 | pub struct Arc { 548 | ptr: NonNull>, 549 | } 550 | 551 | unsafe impl Send for Arc {} 552 | unsafe impl Sync for Arc {} 553 | 554 | pub struct Weak { 555 | ptr: NonNull>, 556 | } 557 | 558 | unsafe impl Send for Weak {} 559 | unsafe impl Sync for Weak {} 560 | ``` 561 | 562 | 因为我们正在优化我们的实现,我们也能通过使用 `std::mem::ManuallyDrop` 来稍微减小 `ArcData` 的大小。我们使用 `Option` 是为了能够在丢弃数据时,将 `Some(T)` 替换为 None,但实际上我们并不需要单独的 None 状态去告诉我们数据消失了,因为 `Arc` 的存在或者缺失已经告诉我们这一点。`ManuallyDrop` 占用了与 T 相同的数量的空间,但是这允许我们任意时刻通过不安全地调用 `ManuallyDrop::drop()` 来手动丢弃 T: 563 | 564 | ```rust 565 | use std::mem::ManuallyDrop; 566 | 567 | struct ArcData { 568 | /// `Arc` 的数量 569 | data_ref_count: AtomicUsize, 570 | /// `Weak` 的数量,如果有任意的 `Arc` 的数量,加上 1。 571 | alloc_ref_count: AtomicUsize, 572 | /// 持有的数据。如果仅剩下 weak 指针,就丢弃它。 573 | data: UnsafeCell>, 574 | } 575 | ``` 576 | 577 | `Arc::new()` 函数几乎持不变,像之前一样初始化两个计数器,但是现在使用 `ManuallyDrop::new()`,而不是 `Some()`: 578 | 579 | ```rust 580 | impl Arc { 581 | pub fn new(data: T) -> Arc { 582 | Arc { 583 | ptr: NonNull::from(Box::leak(Box::new(ArcData { 584 | alloc_ref_count: AtomicUsize::new(1), 585 | data_ref_count: AtomicUsize::new(1), 586 | data: UnsafeCell::new(ManuallyDrop::new(data)), 587 | }))), 588 | } 589 | } 590 | 591 | // … 592 | } 593 | ``` 594 | 595 | Deref 的实现不能再在 Weak 类型上使用私有数据方法,因此我们将在 `Arc` 上添加相同的私有辅助函数: 596 | 597 | ```rust 598 | impl Arc { 599 | // … 600 | 601 | fn data(&self) -> &ArcData { 602 | unsafe { self.ptr.as_ref() } 603 | } 604 | 605 | // … 606 | } 607 | 608 | impl Deref for Arc { 609 | type Target = T; 610 | 611 | fn deref(&self) -> &T { 612 | // 安全性:因为有一个 Arc 包裹 data, 613 | // data 存在,并且可能被共享。 614 | unsafe { &*self.data().data.get() } 615 | } 616 | } 617 | ``` 618 | 619 | `Weak` 的克隆和 `Drop` 实现与我们上次实现完全相同。包括私有的 `Weak::data` 辅助函数,这里是为了完整性: 620 | 621 | ```rust 622 | impl Weak { 623 | fn data(&self) -> &ArcData { 624 | unsafe { self.ptr.as_ref() } 625 | } 626 | 627 | // … 628 | } 629 | 630 | impl Clone for Weak { 631 | fn clone(&self) -> Self { 632 | if self.data().alloc_ref_count.fetch_add(1, Relaxed) > usize::MAX / 2 { 633 | std::process::abort(); 634 | } 635 | Weak { ptr: self.ptr } 636 | } 637 | } 638 | 639 | impl Drop for Weak { 640 | fn drop(&mut self) { 641 | if self.data().alloc_ref_count.fetch_sub(1, Release) == 1 { 642 | fence(Acquire); 643 | unsafe { 644 | drop(Box::from_raw(self.ptr.as_ptr())); 645 | } 646 | } 647 | } 648 | } 649 | ``` 650 | 651 | 现在我终于来到这个新的优化实现的重点内容——克隆 `Arc` 现在只需要操作一个计数器: 652 | 653 | ```rust 654 | impl Clone for Arc { 655 | fn clone(&self) -> Self { 656 | if self.data().data_ref_count.fetch_add(1, Relaxed) > usize::MAX / 2 { 657 | std::process::abort(); 658 | } 659 | Arc { ptr: self.ptr } 660 | } 661 | } 662 | ``` 663 | 664 | 类似地,丢弃 `Arc` 现在也只需要递减一个计数器,除了看到最后一个 `drop` 操作会将计数器从 1 递减到 0。在这中情况下,weak 指针计数也需要递减,以便在没有 weak 指针时到达 0。我们通过简单地创建一个无关紧要的 `Weak`,然后立即丢弃它来实现这一点: 665 | 666 | ```rust 667 | impl Drop for Arc { 668 | fn drop(&mut self) { 669 | if self.data().data_ref_count.fetch_sub(1, Release) == 1 { 670 | fence(Acquire); 671 | // 安全性:data 引用计数是 0, 672 | // 所以没有东西再访问 data。 673 | unsafe { 674 | ManuallyDrop::drop(&mut *self.data().data.get()); 675 | } 676 | // 现在,没有 `Arc` 了, 677 | // 丢弃所有表示 `Arc` 的隐式 weak 指针。 678 | drop(Weak { ptr: self.ptr }); 679 | } 680 | } 681 | } 682 | ``` 683 | 684 | `Weak` 上的 upgrade 方法基本保持不变,只是不再克隆 weak 指针,因为它不再需要递增 weak 计数器。仅当内存分配中至少有一个 `Arc` 才会成功,这意味着 Arc 对象已经计入了 weak 计数器。 685 | 686 | ```rust 687 | impl Weak { 688 | // … 689 | 690 | pub fn upgrade(&self) -> Option> { 691 | let mut n = self.data().data_ref_count.load(Relaxed); 692 | loop { 693 | if n == 0 { 694 | return None; 695 | } 696 | assert!(n <= usize::MAX / 2); 697 | if let Err(e) = 698 | self.data() 699 | .data_ref_count 700 | .compare_exchange_weak(n, n + 1, Relaxed, Relaxed) 701 | { 702 | n = e; 703 | continue; 704 | } 705 | return Some(Arc { ptr: self.ptr }); 706 | } 707 | } 708 | } 709 | ``` 710 | 711 | 到目前为止,这与我们之前的实现差距是非常小的。然而,问题出现在我们仍然要实现最后两个方法:`downgrade` 和 `get_mut`。 712 | 713 | 与之前不同,`get_mut` 方法现在需要检查是否都设置为 1,以判断是否只存在一个 `Arc` 以及没有`Weak`,因为一个 weak 指针计数现在可以表示多个 `Arc` 指针。读取计数器是发生在(稍微)不同时间的两个分开的操作,所以我们不得不非常小心,以确保不会错过任何并发的 downgrade,就像我们在[“优化”](#优化)一节示例的开头所见。 714 | 715 | 如果我们首先检查 `data_ref_count` 是 1,那么我们在检查另一个计数器之前,可能错过随后的 `upgrade()`。但是,如果我们首先检查 `alloc_ref_count` 是 1,那么在检查另一个计数器之前,可能错过随后的 `downgrade()`。 716 | 717 | 摆脱这个困境的方法是通过“锁定”weak 指针计数器来暂时阻塞 `downgrade()` 操作。为此,我们不需要像 mutex 那样的东西。我们可以使用一个特殊的值,如 `usize::MAX`,来表示 weak 指针计数器的特殊“锁定”状态。它只会在加载另一个计数器之前很短暂地被锁定,因此 downgrade 方法只需在它解锁之前自旋,以防止在正好与 `get_mut` 并发运行的极端情况下出现问题。 718 | 719 | 因此,在 `get_mut` 方法中,我们首先需要检查 `alloc_ref_count` 是否为 1,并在确实为 1 的情况下将其替换为 `usize::MAX`。这是 compare_exchange 的任务。 720 | 721 | 然后,我们需要检查其他计数器是否也为 1,之后我们可以立即解锁 weak 指针计数器。如果第二个计数器也为 1,我们就能知道我们有独占访问内存分配和数据的权限,可以返回一个 `&mut T`。 722 | 723 | ```rust 724 | pub fn get_mut(arc: &mut Self) -> Option<&mut T> { 725 | // Acquire 与 Weak::drop 的 Release 递减操作匹配,以确保任意的 726 | // 指针升级在下一个 data_ref_count.load 中可见。 727 | if arc.data().alloc_ref_count.compare_exchange( 728 | 1, usize::MAX, Acquire, Relaxed 729 | ).is_err() { 730 | return None; 731 | } 732 | let is_unique = arc.data().data_ref_count.load(Relaxed) == 1; 733 | // Release 与 `downgrade` 中的 Acquire 操作匹配,以确保任意 734 | // 在 `downgrade` 之后对 data_ref_count 的改变都不会 735 | // 改变以上 is_unique 的结果。 736 | arc.data().alloc_ref_count.store(1, Release); 737 | if !is_unique { 738 | return None; 739 | } 740 | // Acquire 去匹配 Arc::drop 的 Release 递减操作,以确保没有 741 | // 其他东西正在访问 data。 742 | fence(Acquire); 743 | unsafe { Some(&mut *arc.data().data.get()) } 744 | } 745 | ``` 746 | 747 | 正如你所预期的那样,锁定操作(compare_exchange)将使用 `Acquire` 内存排序,而解锁操作(store)将使用 `Release` 内存排序。 748 | 749 | 750 | 如果我们为 `compare_exchange` 操作使用 `Relaxed` 内存排序,那么在从 `data_ref_count` 加载时,可能无法看到新升级的 `Weak` 指针的新值,尽管 `compare_exchange` 已经确认每个 `Weak` 指针都已经被丢弃。 751 | 752 | 如果我们为 store 操作使用 `Relaxed` 内存排序,那么之前的 load 操作可能会观察到未来的 `Arc::drop` 结果,而该 `Arc` 仍然可以降级。 753 | 754 | `Acquire` 屏障与之前相同:它与 `Arc::Drop` 中的 `release-decrement` 操作同步,以确保通过之前的 Arc 克隆的每次访问都发生在新的独占访问之前。 755 | 756 | 最后一部分是 `downgrade` 方法,它将检查特殊的 `usize::MAX` 值,以查看 weak 指针计数器是否被锁定,并在解锁之前自旋等待。就像在 upgrade 实现中一样,我们将在递增之前使用「比较并交换」循环来检查特殊值和溢出: 757 | 758 | ```rust 759 | pub fn downgrade(arc: &Self) -> Weak { 760 | let mut n = arc.data().alloc_ref_count.load(Relaxed); 761 | loop { 762 | if n == usize::MAX { 763 | std::hint::spin_loop(); 764 | n = arc.data().alloc_ref_count.load(Relaxed); 765 | continue; 766 | } 767 | assert!(n <= usize::MAX / 2); 768 | // Acquire 与 get_mut 的 release-store 操作同步。 769 | if let Err(e) = 770 | arc.data() 771 | .alloc_ref_count 772 | .compare_exchange_weak(n, n + 1, Acquire, Relaxed) 773 | { 774 | n = e; 775 | continue; 776 | } 777 | return Weak { ptr: arc.ptr }; 778 | } 779 | } 780 | ``` 781 | 782 | 783 | 我们为 `compare_exchange_weak` 操作使用 `acquire` 内存排序,它与 `get_mut` 函数中的 `release-store` 同步。否则,可能会出现在 `get_mut` 函数解锁计数器之前,后续的 `Arc::drop` 操作的效果对正在运行 `get_mut` 的线程可见。 784 | 785 | 换句话说,在这里,acquire 的「比较并交换」操作有效地“锁定”了 get_mut,阻止其成功。后续的 `Weak::drop` 操作可以使用 `release` 内存排序将计数器递减回 1,从而有效地“解锁”。 786 | 787 | > 我们刚刚制作的 `Arc` 和 `Weak` 的优化实现与 Rust 标准库中包含的实现几乎相同。 788 | 789 | 如果我们运行与以前完全相同的测试([“测试它”](#测试它2)),我们看到这个优化的实现也会编译并通过我们的测试。 790 | 791 | > 如果你觉得为这个优化的实现做出正确的内存排序决定很困难,请不要担心。许多并发数据结构比这个更容易正确地实现。本章的 Arc 实现,特别是因为它在内存排序方面具有棘手的微妙之处。 792 | 793 | ## 总结 794 | 795 | (英文版本) 796 | 797 | * `Arc` 提供一个引用计数分配的共享所有权。 798 | * 通过检查引用计数是否确实是一个 `Arc`,可以有条件地提供独占访问(`&mut T`)。 799 | * 递增原子引用计数可以使用 relaxed 操作,但是最终的递减必须与之前的递减同步。 800 | * *weak 指针*(`Weak`)可以用于避免循环。 801 | * `NonNull` 类型表示一个指向 T 的指针,但是从不为空。 802 | * `ManuallyDrop` 类型可以用于使用不安全代码时,手动决定何时丢弃 T。 803 | * 一旦涉及一个以上的原子变量,事情就会变得更加复杂。 804 | * 实现特定的(自旋)锁有时可能是同时对多个原子变量进行操作的有效策略。 805 | 806 |

807 | 下一篇,第七章:理解处理器 808 |

809 | -------------------------------------------------------------------------------- /8_Operating_System_Primitives.md: -------------------------------------------------------------------------------- 1 | # 第八章:操作系统原语 2 | 3 | (英文版本) 4 | 5 | 目前,我们主要聚焦在非阻塞的操作中。如果我们想要实现一些类似互斥锁或者条件变量的内容,也就是能够等待另一个线程去解锁或者通知它的内容,我们需要一种有效地阻塞当前线程的方式。 6 | 7 | 正如我们在[第四章](./4_Building_Our_Own_Spin_Lock.md)所见到的,我们可以不依赖操作系统,通过自旋,重复地一遍又一遍地尝试某些操作,自己实现阻塞,但这浪费大量的处理器时间。如果我们想要高效地进行阻塞,我们需要操作系统内核的帮助。 8 | 9 | 内核,或者更具体地说是其中的调度部分,负责决定哪个进程或者线程在何时运行,运行多长时间,并且在哪个处理器核心运行。尽管线程在等待某个事件发生时,内核可以停止,并给它任意的处理器时间,优先考虑其他能更好地利用这个有限资源的线程。 10 | 11 | 我们将需要一种方式来通知内核我们正在等待某个事件,并要求它将我们的线程置于睡眠状态,直到发生相关的事情。 12 | 13 | ## 使用内核接口 14 | 15 | (英文版本) 16 | 17 | 与内核进行通信的方式很大程度依赖于操作系统,甚至是它的版本。通常,如何工作的细节被一个库或者更多库所隐藏,这些库为我们处理这些细节。例如,使用 Rust 的标准库,我们可以仅调用 `File::open()` 去打开这个文件,而不必关心任何操作系统内核的细节。类似地,使用 C 标准库(`libc`)也可以调用标准的 `fopen()` 函数去打开一个文件。调用这样的函数最终会导致调用操作系统内核,也称为*系统调用*(syscall),通常通过专门的处理器指令来完成(在某些架构上,该指令甚至直接称为 syscall)。 18 | 19 | 通常期望程序(有时直接要求)不直接进行系统调用,而是利用操作系统携带的更高级别的库。在 Unix 系统中(例如那些基于 Linux 的),libc 扮演了与内核交换的标准接口的特殊角色。 20 | 21 | POSIX[^1](可移植操作系统接口)标准,包括了在类 Unix 系统上的 libc,以及对其额外的要求。例如,在 C 标准的 `fopen()` 函数之外,POSIX 还要求存在更低级别的 `open()` 和 `openat()` 函数来打开文件,这些函数通常直接对应一个系统调用。由于 libc 在类 Unix 系统上的特殊地位,使用其他语言编写的程序通常仍然使用 libc 来进行与内核的所有交互。 22 | 23 | Rust 软件,包括标准库,通常通过相同名称的 libc crate 使用 libc 库。 24 | 25 | 尤其对于 Linux,系统调用接口被保证稳定,允许我们直接进行系统调用,而不使用 libc。尽管这不是最常见或最推荐的方式,但它正在逐渐变得更受欢迎。 26 | 27 | 然而,虽然 MacOS 也是一个 Unix 操作系统,跟随 POSIX 标准,但是它的内核系统调用接口并不稳固,并且我们并不建议直接使用它。程序被允许使用的唯一稳定接口是通过系统附带的库(如 libc、libc++)和其他库(用于 C、C++、Objective-C 和 Swift)提供的接口,这些是苹果公司的首选编程语言。 28 | 29 | Windows 不遵循 POSIX 标准。它并没有携带一个拓展的 libc 作为主要的内核接口,而是携带了一系列独立的库,例如 `kernel32.dll`,它提供了 Windows 的特定功能,如用于打开文件的 `CreateFileW`。与在 macOS 上一样,我们不应使用未记录的较低级别函数或直接进行系统调用。 30 | 31 | 通过它们的库,操作系统为我们提供了需要与内核进行交互的同步原语,如互斥锁和条件变量。这些实现的哪一部分属于库/内核的一部分,在不同的操作系统中有很大的差异。例如,有时互斥锁的锁定和解锁操作直接对应一个内核系统调用,而在其他系统中,库会处理大部分操作,并且只在需要阻塞或唤醒线程时执行系统调用(后者往往更高效,因为进行系统调用可能较慢)。 32 | 33 | ## POSIX 34 | 35 | (英文版本) 36 | 37 | 作为 POSIX 线程扩展的一部分,更为人熟知的是 pthread,POSIX 规范了用于并发的数据类型和函数。尽管 libthread 在技术上是作为一个独立的系统库的一部分,但是如今它通常被直接包含在 libc 中。 38 | 39 | 除了线程的 spawn 和 join 功能(`pthread_create` 和 `pthread_join`)外,pthread 还提供了最常见的同步原语:互斥锁(`pthread_mutex_t`)、读写锁(`pthread_rwlock_t`)和条件变量(`pthread_cond_t`)。 40 | 41 | * *pthread_mutex_t* 42 | 43 | Pthread 的互斥锁必须通过调用 `pthread_mutex_init()` 进行初始化,并使用 `pthread_mutex_destroy()` 进行销毁。初始化函数接收一个 `pthread_mutexattr_t` 类型的参数,该参数可用于配置互斥锁的某些属性。 44 | 45 | 其中一个属性是互斥锁在*递归锁定*时的行为,其是指同一线程再次尝试锁定已经持有的锁时发生的情况。在默认设置(`PTHREAD_MUTEX_DEFAULT`)下使用递归锁定会导致未定义的行为,但也可以配置为产生错误(`PTHREAD_MUTEX_ERRORCHECK`)、死锁(`PTHREAD_MUTEX_NORMAL`)或成功的第二次锁定(`PTHREAD_MUTEX_RECURSIVE`)。 46 | 47 | 通过 `pthread_mutex_lock()` 或 `pthread_mutex_trylock()` 来锁定这些互斥锁,通过 `pthread_mutex_unlock()` 来解锁。此外,与 Rust 的标准互斥锁不同的是,它们还支持通过 `pthread_mutex_timedlock()` 在有限的时间内进行锁定。 48 | 49 | 可以通过分配值 `PTHREAD_MUTEX_INITIALIZER` 来静态初始化 `pthread_mutex_t`,而无需调用 `pthread_mutex_init()`。但是,这仅适用于具有默认设置的互斥锁。 50 | 51 | * *pthread_rwlock_t* 52 | 53 | Pthread 的读写锁通过 `pthread_rwlock_init()` 和 `pthread_rwlock_destroy()` 进行初始化和销毁。与互斥锁类似,默认的 `pthread_rwlock_t` 可以使用 `PTHREAD_RWLOCK_INITIALIZER` 静态地初始化。 54 | 55 | 与 pthread 互斥锁相比,pthread 读写锁通过它的初始化函数可配置的属性要少得多。特别要注意的是,尝试递归写锁定将始终导致死锁。 56 | 57 | 然而,尝试递归获取额外的读锁是保证会成功的,即使有 writer 正在等待。这实际上排除了任何优先考虑 writer 而不是 reader 的高效实现,这就是为什么大多数 pthread 实现优先考虑 reader 的原因。 58 | 59 | 它的接口与 `pthread_mutex_t` 几乎相同,包括支持时间限制,除了每个锁定函数都有两个变体:一个用于 reader(`pthread_rwlock_rdlock`),一个用于 writer(`pthread_rwlock_wrlock`)。也许令人惊讶的是,仅有一个解锁函数(`pthread_rwlock_unlock`),其用于解锁任一类型的锁。 60 | 61 | * *pthread_cond_t* 62 | 63 | pthread 条件变量与 pthread 互斥锁一起使用。通过 `pthread_cond_init` 和 `pthread_cond_destroy` 进行初始化和销毁,并且可以配置一些属性。其中最值得注意的是,我们可以配置时间限制使用单调时钟[^2](类似于 Rust 的 Instant)还是实时时钟[^3](类似于 Rust 的 SystemTime,有时称为“挂钟时间”)。具有默认设置的条件变量(例如由 `PTHREAD_COND_INITIALIZER` 静态初始化的条件变量)使用实时时钟。 64 | 65 | 通过 `pthread_cond_timedwait()` 等待此类条件变量,可选择设置时间限制。通过调用 `pthread_cond_signal()` 唤醒等待的线程,或者为了一次唤醒所有等待的线程,调用 `pthread_cond_broadcast()`。 66 | 67 | Pthread 提供的其余同步原语是屏障(`pthread_barrier_t`)、自旋锁(`pthread_spinlock_t`)和一次性初始化(`pthread_once_t`),我们不会讨论。 68 | 69 | ### 在 Rust 中包装类型 70 | 71 | (英文版本) 72 | 73 | 通过方便地将其 C 类型(通过 libc crate)包装在 Rust 结构体中,我们可以轻松地将这些 pthread 同步原语暴露给 Rust,例如: 74 | 75 | ```rust 76 | pub struct Mutex { 77 | m: libc::pthread_mutex_t, 78 | } 79 | ``` 80 | 81 | 然而,这种方法存在一些问题,因为该 pthread 类型是为 C 设计的,而不是为 Rust 设计的。 82 | 83 | 首先,Rust 关于可变性和借用有一些规则,通常不允许在共享时进行修改。由于类似 `pthread_mutex_lock` 这样的函数总是可能对互斥锁进行修改,我们将需要内部可变性来确保这是可接受的。因此,我们需要将其包装在 UnsafeCell 中: 84 | 85 | ```rust 86 | pub struct Mutex { 87 | m: UnsafeCell, 88 | } 89 | ``` 90 | 91 | 一个巨大的问题是关于*移动*。 92 | 93 | 在 Rust 中,我们所有时间都在移动对象。例如,通过从函数返回对象、将其作为参数传递或者简单地将其分配给新的位置。我们拥有的多所有东西(并且没有被其他东西借用),我们可以自由地移动它们到一个新位置。 94 | 95 | 然而,在 C 中,这并不是普遍正确的。在 C 中,类型通常依赖它的内存地址保持不变。例如,它可能包含一个指向自身的指针,或者在某个全局数据结构中存储一个指向自己的指针。在这种情况下,移动到一个新的位置可能导致未定义行为。 96 | 97 | 我们讨论的 pthread 类型不能保证它们是*可移动的*,在 Rust 中,这会带来很大的问题。即使是一个简单的惯用的 `Mutex::new()` 函数也是一个问题:它将返回一个 mutex 对象,这将移动到一个内存的新位置。 98 | 99 | 因为用户可能总是移动任何它们拥有的 mutex 对象到其他地方,我们要么需要承诺它别这么做,要么使接口不安全;或者我们需要取走所有权并且隐藏所有内容到一个包装类型后面(可以使用 `std::pin::Pin` 来完成)。这些都不是最好的解决方案,因为它们会影响我们的 mutex 类型的接口,使其用起来容易出错和/或不方便使用。 100 | 101 | 一个可以的解决方案是将 mutex 包装在一个 Box 中。通过将 pthread 的 mutex 放在它自己的内存分配中,即使所有者被移动,它仍然在内存中的相同位置。 102 | 103 | ```rust 104 | pub struct Mutex { 105 | m: Box>, 106 | } 107 | ``` 108 | 109 | > 这就是 `std::sync::Mutex` 在 Rust 1.62 之前在所有 Unix 平台上实现的方式。 110 | 111 | 这个方式的缺点就是开销大:每个 mutex 都有自己的内存分配,为创建、销毁以及使用 mutex **增加**了显著的开销。另一个缺点是它阻止了 new 函数编译时执行(`const`),这妨碍了拥有静态 mutex 的方式。 112 | 113 | 即使 `pthread_mutex_t` 是可移动的,`const fn new` 也可能仅使用默认设置来初始化,这导致了当递归锁定时的未定义行为。没有办法设计一个安全的接口来防止递归锁定,因此这意味着我们要使用 unsafe 标记锁定函数,以使用户承诺他们不会这样做。 114 | 115 | 当丢弃 mutex 时,在我们的 Box 方法中仍然存在一个问题。看起来,如果设计正确,就不可能在被锁定时丢弃 mutex,因为通过 MutexGuard 借用它时,不可能丢弃它。MutexGuard 必须先被丢弃,解锁 Mutex。然而,在 Rust 中,安全地遗忘(或泄露)一个对象,而不将其丢弃是安全的。这意味着可以编写类似下面的代码: 116 | 117 | ```rust 118 | fn main() { 119 | let m = Mutex::new(..); 120 | 121 | let guard = m.lock(); // 锁定它 .. 122 | std::mem::forget(guard); // .. 但是没有解锁它。 123 | } 124 | ``` 125 | 126 | 在以上示例中,`m` 将在作用域结束后被丢弃,尽管它仍然被锁定。根据 Rust 的编译器来看,这是好的,因为 guard 已经被泄漏并且不能再使用。 127 | 128 | 然而,pthread 规定在已锁定的 mutex 调用 `pthread_mutex_destroy()` 并不能保证工作并且可能导致未定义行为。一种解决方案是当丢弃我们的 Mutex 时,首先试图去锁定(和解锁)pthread mutex,并且当它已经锁定时触发 panic(或泄漏 Box),但这甚至要更大的开销。 129 | 130 | 这些问题不仅适用于 `pthread_mutex_t`,还适用于我们讨论的其他类型。总体而言,pthread 的同步原语设计对 C 是好的,但是并不完全适合 Rust。 131 | 132 | ## Linux 133 | 134 | 英文版本 135 | 136 | 在 Linux 系统中,pthread 同步原语所有都是使用 *futex 系统调用*实现。它的名称来自“快速用户区互斥[^6]”(fast user-space mutex),因为**增加**这个系统调用最初的动机就是允许库(如 pthread 实现)包含一个快速且高效 mutex 实现。它的灵活远不止于此,可以用来构建许多不同的同步工具。 137 | 138 | 在 2003 年,futex 系统调用被增加到 Linux 内核,此后进行了几次改善和扩展。一些其他的系统调用因此也**增加**了相似的功能,更值得注意的是,在 2012 年 Windows 8 也**增加**了 WaitOnAddress(我们将会稍后在[“Windows”](#windows)部分讨论这个)。在 2020 年,C++ 语言甚至把基础的类 futex 操作**增加**到了标准库,并添加了 `atomic_wait` 和 `atomic_notify` 函数。 139 | 140 | ### Futex 141 | 142 | 英文版本 143 | 144 | 在 Linux 上,`SYS_futex` 是一个系统调用,在 32 位的原子整数上它实现了各种操作。主要的两个操作是 `FUTEX_WAIT` 和 `FUTEX_WAKE`。等待操作会让线程进入睡眠状态,而在同一个原子变量上进行唤醒操作则会将线程唤醒。 145 | 146 | 这些操作并不会在原子整数中存储任何内容。相反,内核会记住哪些线程正在等待哪个内存地址,以便唤醒操作能够正确地唤醒线程。 147 | 148 | 在[第一章的“等待:阻塞和条件变量”](./1_Basic_of_Rust_Concurrency.md#等待-阻塞park和条件变量)中,我们看到其他阻塞和唤醒线程的机制,需要一种方式以确保唤醒操作不会在竞争中丢失。对于线程的阻塞操作,通过将 `unpark()` 操作应用于未来的 `park()` 操作,来解决这个问题。并且对于条件变量来说,这是通过与条件变量一起使用的互斥锁来解决的。 149 | 150 | 对于 futex 的等待和唤醒操作,使用了另一种机制。等待操作接受一个参数,该参数是我们期望原子变量具有的值,如果不匹配,就会拒绝阻塞。等待操作在与唤醒操作的原子性上保持一致,这意味着在检查期望值和实际进入睡眠状态之间,不会丢失任何唤醒信号。 151 | 152 | 如果我们确保在唤醒操作之前改变原子变量的值,我们就可以确保即将开始等待的线程不会进入睡眠状态,这样就不再关心可能丢失 futex 唤醒操作的问题了。 153 | 154 | 让我们通过一个简单的例子来实践一下。 155 | 156 | 首先,我们需要能够调用这些系统调用。我们可以使用 libc crate 中的 syscall 函数来实现,并将每个调用封装在一个方便的 Rust 函数中,如下所示: 157 | 158 | ```rust 159 | #[cfg(not(target_os = "linux"))] 160 | compile_error!("Linux only. Sorry!"); 161 | 162 | pub fn wait(a: &AtomicU32, expected: u32) { 163 | // 参考 futex (2) 手册中的系统调用签名 164 | unsafe { 165 | libc::syscall( 166 | libc::SYS_futex, // futex 系统调用 167 | a as *const AtomicU32, // 要操作的原子 168 | libc::FUTEX_WAIT, // futex 操作 169 | expected, // 预期的值 170 | std::ptr::null::(), // 没有超时 171 | ); 172 | } 173 | } 174 | 175 | pub fn wake_one(a: &AtomicU32) { 176 | // 参考 futex (2) 手册中的系统调用签名 177 | unsafe { 178 | libc::syscall( 179 | libc::SYS_futex, // futex 系统调用 180 | a as *const AtomicU32, // 要操作的原子 181 | libc::FUTEX_WAKE, // futex 操作 182 | 1, // 要唤醒的线程数量 183 | ); 184 | } 185 | } 186 | ``` 187 | 188 | 现在,作为一个使用示例,让我们用这些让一个线程等待另一个线程。我们将使用一个原子变量,我们用 0 为它初始化,主线程将在该变量上进行 futex 等待。第二个线程会将变量更改为 1,然后在上面运行 futex 唤醒操作以唤醒主线程。 189 | 190 | 就像线程阻塞和等待一个条件变量,futex 等待操作可能甚至在没有任何发生的情况下虚假唤醒。因此,通常在循环中使用它,如果我们等待的条件尚未满足,就会重复它。 191 | 192 | 让我们来看一下下面的示例: 193 | 194 | ```rust 195 | fn main() { 196 | let a = AtomicU32::new(0); 197 | 198 | thread::scope(|s| { 199 | s.spawn(|| { 200 | thread::sleep(Duration::from_secs(3)); 201 | a.store(1, Relaxed); // 1 202 | wake_one(&a); // 2 203 | }); 204 | 205 | println!("Waiting..."); 206 | while a.load(Relaxed) == 0 { // 3 207 | wait(&a, 0); // 4 208 | } 209 | println!("Done!"); 210 | }); 211 | } 212 | ``` 213 | 214 | 1. 在几秒钟后,创建的线程将设置原子变量的值为 1。 215 | 2. 然后,它执行一个 futex 唤醒操作去唤醒主线程,以防止它正在睡眠,这样可以看到变量已经发生了变化。 216 | 3. 主线程将会等待直到变量是 0,然后继续打印最终的消息。 217 | 4. futex 的 `wait` 操作用于将线程置入睡眠状态。非常重要的是,在进入睡眠之前,此操作将检查变量是否仍然是 0,这是在步骤 3 和步骤 4 之间不能丢失来自产生线程的信号的原因。要么 1(并且因此 2)尚未发生,它将进入睡眠状态,要么 1(并且可能 2)已经发生,线程将立即继续执行。 218 | 219 | 在这里一个重要的观察是,如果 a 已经在 while 循环之前设置为 1,那么就可以完全避免等待调用。以类似地方式,如果主线程还在原子变量中存储了它是否开始等待的信号(通过将其设置为除了 0 或 1 之外的值),如果主线程尚未开始等待,发送信号的线程可以跳过 futex 的等待操作。这就是基于 futex 的同步原语如此快速的原因:由于我们自己管理状态,除非我们真正的需要阻塞,否则我们不需要依赖内核。 220 | 221 | > 自 Rust 1.48 以来,在 Linux 上,标准库的线程阻塞(park)函数是这样实现的。它们每个线程使用一个原子变量,有三种可能的状态:0 表示空闲和初始状态、1 为“已释放但尚未阻塞”,-1 为“已阻塞但尚未释放”。 222 | 223 | 在[第九章](./9_Building_Our_Own_Locks.md),我们将使用这些操作实现互斥锁、条件变量以及读写锁。 224 | 225 | ### Futex 操作 226 | 227 | 英文版本 228 | 229 | 接下来到等待和唤醒操作,futex 系统调用还支持其他几个操作。在该章节,我们将简要地讨论此系统调用的每个支持的操作。 230 | 231 | futex 的第一个参数始终是指向要操作的 32 位原子变量的指针。第二个参数是一个表示操作的常量,例如 `FUTEX_WAIT``,还可以添加最多两个标识:FUTEX_PRIVATE_FLAG` 和/或 `FUTEX_CLOCK_REALTIME`,我们将在下面进行讨论。剩余的参数取决于具体的操作,我们将在每个操作的描述中进行说明。 232 | 233 | * *FUTEX_WAIT* 234 | 235 | 该操作接收 2 个额外的参数:期待原子变量具有的值和指向表示最长时间等待的 timespec 的指针。 236 | 237 | 如果原子变量的值匹配预期的值,等待操作将会阻塞,直到被其中一个唤醒操作唤醒,或者直到传递的 timespec 持续时间过去。如果 timespec 的指针为 null,则没有时间限制。此外,等待操作可能会在达到时间限制之前出现虚假唤醒,并返回没有相应的唤醒操作。 238 | 239 | 与其他 `futex` 操作相比,检查和阻塞操作是单个原子操作,这意味着它们之间不会丢失唤醒信号。 240 | 241 | timespec 指定的持续时间默认代表单调时钟(如 Rust 的 Instant)上的持续时间。通过添加 `FUTEX_CLOCK_REALTIME` 标识,将使用实时时钟(如 Rust 的 SystemTime)。 242 | 243 | 返回值指示是否匹配预期值以及是否达到了超时。 244 | 245 | * *FUTEX_WAKE* 246 | 247 | 此操作需要 1 个额外的参数:要唤醒的线程数,使用 i32 类型。 248 | 249 | 这会唤醒指定数量的,在相同原子变量上的等待操作中被阻塞的线程。(如果没有很多等待的线程,则唤醒较少的线程)更常见的是,这个参数要么只唤醒一个线程,要么是设置为 `i32::MAX` 唤醒所有线程。 250 | 251 | 返回值是唤醒的线程数。 252 | 253 | * *FUTEX_WAIT_BITSET* 254 | 255 | 这个操作接收 4 个额外的参数:期待原子变量具有的值、指向表示最长时间等待的 timespec 指针、一个忽略的指针以及一个 32 位“bitset”(u32)。 256 | 257 | 该操作行为与 FUTEX_WAIT 相同,但是有两点区别。 258 | 259 | 第一个区别是它接收一个 bitset 参数,可以仅用于等待特定的唤醒操作,而不是在相同的原子变量上的所有唤醒操作等待。`FUTEX_WAKE` 操作从不会被忽略,但是如果等待的“bitset”和唤醒的“bitset”没有一位是相等的,则忽略来自 `FUTEX_WAKE_BITSET` 操作的信号。 260 | 261 | 例如,`FUTEX_WAKE_BITSET` 操作的“bitset”是 `0b0101`,它能唤醒位集为 `0b1100` 的 `FUTEX_WAIT_BITSET` 操作,但是不能唤醒的位集为 `0b0010`。 262 | 263 | 这在实现类似读写锁的时候很有用,可以唤醒 writer,而不唤醒任何 reader。然而,请注意,对于处理两种不同类型的 writer,使用两个单独的原子变量可能比使用一个更高效,因为内核将针对每个原子变量维护一个等待者(waiter)列表。 264 | 265 | `FUTEX_WAIT_BITSET` 与 `FUTEX_WAIT` 的另一个区别是,它使用 `timespec` 作为绝对时间戳,而不是持续时间。因此,通常会将 `FUTEX_WAIT_BITSET` 与 `u32::MAX`(所有位都为 1)的“bitset”一起使用,从而将其转变为常规的 FUTEX_WAIT 操作,但设置了绝对时间戳作为等待的时间限制。 266 | 267 | * *FUTEX_WAKE_BITSET* 268 | 269 | 此操作接收了 4 个额外的参数:要唤醒的线程数量、2 个忽略的指针以及 32 位“bitset”(u32)。 270 | 271 | 此操作与 FUTEX_WAKE 操作相同,只是它不会唤醒那些“bitset”没有重复的 FUTEX_WAIT_BITSET 操作。(见上面的 FUTEX_WAIT_BITSET。) 272 | 273 | 当 bitset 设置为 `u32::MAX`(所有位都为 1)时,这与 FUTEX_WAKE 操作相同。 274 | 275 | * *FUTEX_REQUEUE* 276 | 277 | 此操作接收 3 个额外的参数:要唤醒的线程数(i32)、要重新排队的线程数(i32)和一个次要原子变量的地址。 278 | 279 | 该操作唤醒一个给定数量的等待线程,并且将剩余的等待线程重新排队,一等待另一个原子变量。 280 | 281 | 重新排队的等待线程继续等待,但不再受到主原子变量的唤醒操作的影响。相反,它们现在通过在次要原子变量上唤醒操作来唤醒。 282 | 283 | 这对于实现类似条件变量的“notify_all”操作是有用的。与其唤醒所有线程,不如随后尝试锁定 mutex,否则很可能导致除了一个线程外的其他线程都在随后立即等待该 mutex,我们可以仅唤醒一个线程,并将其所有其他的线程重新排队,直接让它们等待 mutex 而不先唤醒它们。 284 | 285 | 与 FUTEX_WAKE 操作类似,可以使用 `i32::MAX` 的值来重新排队所有等待的线程。(指定唤醒线程数为 `i32::MAX` 的值并不是非常有用,因为这将使该操作等效于 `FUTEX_WAKE`。) 286 | 287 | 返回值是唤醒线程的数量。 288 | 289 | * *FUTEX_CMP_REQUEUE* 290 | 291 | 此操作接收额外的 4 个参数:要唤醒的线程数(i32)、要重新排队的线程数(i32)、次要原子变量的地址以及主要原子变量预期的值。 292 | 293 | 这个操作与 `FUTEX_REQUEUE` 几乎相同,但如果主要原子变量的值不匹配预期值,它会拒绝执行。对值的检查和重新排队操作在与其他 futex 操作相比是原子的。 294 | 295 | 与 FUTEX_REQUEUE 不同,它返回被唤醒和重新排队的线程数量之和。 296 | 297 | * *FUTEX_WAKE_OP* 298 | 299 | 此操作接收 4 个额外的参数:在主要原子变量上要唤醒的线程数(i32)、在次要原子变量上可能要唤醒的线程数(i32)、次要原子变量的地址以及一个 32 位的值,其用于编码要执行的操作和要进行比较的条件。 300 | 301 | 这是一个非常专业的用于修改次要原子变量的操作,唤醒许多等待著原子变量的线程,检查原子变量的前一个值是否与给定值匹配,如果匹配,则还会唤醒次要原子变量上的一些线程。 302 | 303 | 换句话说,它与下面的代码等同,除了整个操作行为与其他 futex 操作相比是原子的: 304 | 305 | ```rust 306 | let old = atomic2.fetch_update(Relaxed, Relaxed, some_operation); 307 | 308 | wake(atomic1, N); 309 | if some_condition(old) { 310 | wake(atomic2, M); 311 | } 312 | ``` 313 | 314 | 通过系统调用的最后一个参数来指定要执行的修改操作以及要检查的条件,这是一个 32 位编码。操作可以以下之一:赋值、加运算、二进制或、二进制与非、二进制异或,其中包含一个 12 位参数或者是一个 32 位的 2 的幂次方参数。比较操作可以选择 `==`、`!=`、`<`、`<=`、`>` 和 `>=`,并带有一个 12 位参数。 315 | 316 | 有关该参数的编码详细信息,请参阅 futex(2) 手册页,或使用 `crates.io` 上的 linux-futex crate,该 crate 提供了一种方便的构造参数的方法。 317 | 318 | 返回值是唤醒线程的总数。 319 | 320 | 乍看之下,这似乎是一个具有许多用途的灵活操作。然而,它最初设计用于 GNU libc 中的一个特定用例,其中需要从两个单独的原子变量中唤醒两个线程。这个特定的用例已经被不同的实现替代,不再利用 FUTEX_WAKE_OP。 321 | 322 | 可以添加 FUTEX_PRIVATE_FLAG 到其中的任何一个操作,以启用可能的优化。(通常情况下,如果对同一原子变量的所有相关 futex 操作来自同一进程,则可以利用此标识)。为了使用该标识,每个相关的 futex 操作都必须包括相同的标识。通过允许内核假设不会与其他进程发生交互,它可以跳过执行 futex 操作中的一些可能高开销的步骤,从而提高性能。 323 | 324 | 除了 Linux,NetBSD 也支持上述所有的 futex 操作。OpenBSD 也有一个 futex 系统调用,但仅支持 FUTEX_WAIT、FUTEX_WAKE 和 FUTEX_REQUEUE 操作。FreeBSD 没有原生的 futex 系统调用,但包含一个名为 `_umtx_op` 的系统调用,其中包含与 FUTEX_WAIT 和 FUTEX_WAKE 几乎相同的功能:`UMTX_OP_WAIT`(用于 64 位原子变量)、UMTX_OP_WAIT_UINT(用于 32 位原子变量)和 UMTX_OP_WAKE。Windows 也包含与 futex 等待和唤醒操作非常相似的函数,我们将在本章后面讨论。 325 | 326 |
327 |

新的 Futex 操作

328 |

发布在 2022 年的 Linux 5.16,引入了一个新的系统调用:futex_waitv。这个新的系统调用通过向它提供一个包含待等待的原子变量(及其期望值)的列表,允许一次等待多个 futex。在 futex_waitv 上被阻塞的线程可以通过在任意指定的变量上进行唤醒操作来被唤醒。

329 | 330 |

这个新的系统调用还为未来的扩展留出了空间。例如,可以指定待等待的原子变量的大小。虽然最初的实现只支持 32 位原子变量,就像原始的 futex 系统调用一样,但在未来可能会扩展为支持 8 位、16 位和 64 位原子变量。

331 |
332 | 333 | ### 优先继承 Futex 操作 334 | 335 | (英文版本) 336 | 337 | 优先级反转[^7]是指高优先级线程在低优先级线程持有的锁上被阻塞的问题。高优先级线程实际上“反转”了它的优先级,因为它现在必须等待低优先级线程释放锁才能继续执行。 338 | 339 | 解决这个问题的方法是优先级继承,即阻塞的线程继承等待它的最高优先级线程的优先级,在持有锁期间临时提高低优先级线程的优先级。 340 | 341 | 除了我们之前讨论过的七个 futex 操作外,还有六个专门用于实现优先级继承锁的优先级继承 futex 操作。 342 | 343 | 我们之前讨论过的通用 futex 操作对于原子变量的具体内容没有任何要求。我们可以自己选择 32 位的表示方式。然而,对于优先级继承 mutex,内核需要能够理解 mutex 是否被锁定,如果锁定了,则需要知道哪个线程锁定了它。 344 | 345 | 为了避免在每个状态变化上进行系统调用,优先级继承 futex 操作指定了 32 位原子变量的确切内容,以便内核可以理解它:最高位表示是否有任何线程正在等待锁定 mutex,最低的 30 位包含持有锁的线程 ID(Linux 的 tid,而不是 Rust 的 ThreadId),当解锁时为零。 346 | 347 | 作为额外的功能,如果持有锁的线程在未解锁的情况下终止,内核将设置次高位,但前提是没有任何等待着。这使得 mutex 具有*鲁棒性*[^8]:这是一个术语,用于描述 mutex 在“拥有”线程意外终止的情况下能够正常处理的能力。 348 | 349 | 优先级继承 futex 操作与标准 mutex 操作一一对应:FUTEX_LOCK_PI 用于锁定,FUTEX_UNLOCK_PI 用于解锁,FUTEX_TRYLOCK_PI 用于非阻塞锁定。此外,FUTEX_CMP_REQUEUE_PI 和 FUTEX_WAIT_REQUEUE_PI 操作可用于实现与优先级继承 mutex 配对的条件变量。 350 | 351 | 我们将不详细讨论这些操作。有关详细信息,请参阅 futex(2) Linux 手册页或 `crates.io` 上的 linux-futex crate。 352 | 353 | ## macOS 354 | 355 | (英文版本) 356 | 357 | macOS 部分的内核支持各种有用的低级并发相关的系统调用。然而,就像大多数操作系统一样,内核接口并不是稳定的,并且我们应该直接地使用它。 358 | 359 | 软件与 macOS 内核交互的唯一方式是通过系统携带的库。这些库包含它对 C(libc)、C++(libc++)、Objective-C 和 Swift 的标准库实现。 360 | 361 | 作为符合 POSIX 标准的 Unix 系统,macOS C 标准库包含一个完整的 pthread 实现。其他语言中的标准库锁通常在底层使用 pthread 原语。 362 | 363 | 在 macOS 上,与其他系统对比,Pthread 的锁相对低较慢。原因之一是 macOS 上的锁默认情况下是*公平锁*(Fair Lock),这意味着几个线程试图去锁定相同的 mutex 时,它们会按照到达的顺序一次获得锁定,就像一个完美的队列。尽管公平性可能是值得拥有的属性,但它会显著降低性能,特别是在高竞争的情况下。 364 | 365 | ### os_unfair_lock 366 | 367 | (英文版本) 368 | 369 | 除了 pthread 原语,macOS 10.12 引入了一种新的轻量级平台特定的互斥锁,它是不公平的:`os_unfair_lock`。它的大小仅有 32 位,可以使用 OS_UNFAIR_LOCK_INIT 常来那个静态地初始化,并且不需要销毁。它可以通过 `os_unfair_lock_lock()`(阻塞)或 `os_unfair_lock_trylock()`(非阻塞)来锁定它,并且通过 `os_unfair_lock_unlock()` 来解锁。 370 | 371 | 不幸的是,它没有条件变量,也没有 reader-writer 变体。 372 | 373 | ## Windows 374 | 375 | (英文版本) 376 | 377 | Windows 操作系统携带了一系列库,它们一起形成了 *Windows API*,通常称之为“Win32 API”(甚至在 64 位系统也是)。它构成了一个在“Native 之上”的层:大部分是与内核没有交互的接口,我们不建议直接使用它。 378 | 379 | 通过微软官方提供的 windows 和 windows-sys crate,Windows API 可以为 Rust 程序所用,这在 `crates.io` 上是可获得的。 380 | 381 | ### 重量级内核对象 382 | 383 | (英文版本) 384 | 385 | 在 Windows 上可用的许多旧的同步原语完全由内核管理,这使得它们非常重量,并赋予它们与其他内核管理对象(例如文件)类似的属性。它们可以被多个进程使用,可以通过名称进行命名和定位,并且支持细粒度的权限,类似于文件。例如,可以允许一个进程等待某个对象,而不允许它通过该对象发送信号来唤醒其他进程。 386 | 387 | 这些重量级的内核管理同步对象包括 Mutex(可以锁定和解锁)、Event(可以发送信号和等待)以及 WaitableTimer(可以在选择的时间后或定期自动发送信号)。创建这样的对象会得到一个句柄(HANDLE),就像打开一个文件一样,可以轻松地传递并与常规的 HANDLE 函数一起使用,特别是一系列的等待函数。这些函数允许我们等待各种类型的一个或多个对象,包括重量级同步原语、进程、线程和各种形式的 I/O。 388 | 389 | ### 轻量级对象 390 | 391 | (英文版本) 392 | 393 | 在 Windows API 中,一个轻量级的同步原语包括是“临界区[^4]”(critical section)。 394 | 395 | *临界区*这个术语指的是程序的一部分,即代码的“区段”,可能不允许超过一个线程进入。这种保护临界区段机制通常称之为互斥锁。然而,微软为这种机制使用“临界区”的名称,可能因为之前讨论的重量级 mutex 对象已经采用了“互斥锁”这个名称。 396 | 397 | Winodows 中的 `CRITICAL_SECTION` 实际上是一个递归互斥锁,只是它使用了“enter”(进入)和“leave”(离开)而不是“lock”(锁定)和“unlock”(解锁)。作为递归互斥锁,它仅被设计用于保护其他的线程。它允许相同的线程多次锁定(或者“进入”)它,也要求该线程也必须相同的次数解锁(“离开”)。 398 | 399 | 当使用 Rust 包装该类型时,有些东西值得注意。成功地锁定(进入)`CRITICAL_SECTION` 不应该导致对其数据保护的独占引用(`&mut T`)。否则,线程可以使用此来创建对同一数据的两个独占引用,这会立即导致未定义行为。 400 | 401 | CRITICAL_SECTION 使用 `InitializeCriticalSection()` 函数来初始化,使用 `DeleteCriticalSection()` 函数来销毁,并且不能被移动。通过 `EnterCriticalSection()` 或者 `TryEnterCriticalSection()` 来锁定,并且使用 `LeaveCriticalSection()` 解锁。 402 | 403 | > 在 Rust 1.51 之前,Windows XP 上的 `std::sync::Mutex` 基于(Box 的内存分配)CRITICAL_SECTION 对象。(Rust 1.51 放弃了对 Windows XP 的支持。) 404 | 405 | #### 精简的读写(SRW)锁[^5] 406 | 407 | (英文版本) 408 | 409 | 从 Windows Vista(和 Windows Server 2008)开始,Windows API 包含了一个非常轻量级的优秀锁原语:*精简读写锁*,简称 *SRW 锁*。 410 | 411 | SRWLOCK 类型仅是一个指针大小,可以用 `SRWLOCK_INIT` 静态初始化,并且不需要销毁。当不再被使用(借用),我们甚至允许移动它,使它成为 Rust 类型的理想选择。 412 | 413 | 它通过 `AcquireSRWLockExclusive()`、`TryAcquireSRWLockExclusive()` 和 `ReleaseSRWLockExclusive()` 提供了独占(writer)锁定和解锁,并通过 `AcquireSRWLockShared()`、`TryAcquireSRWLockShared()` 和 `ReleaseSRWLockShared()` 提供了共享(reader)锁定和解锁。通常可以将其用作普通的互斥锁,只需忽略共享(reader)锁定函数即可。 414 | 415 | SRW 锁既不优先考虑 writer 也不优先考虑 reader。虽然不能保证,但是它试图去按顺序去服务所有锁请求,以**减少**性能下降。在已经持有一个共享(reader)锁定的线程上不要尝试获取第二个共享(reader)锁定。如果该操作在另一个线程的独占(writer)锁定操作之后排队,那么这样做可能会导致永久死锁,因为第一个线程已经持有的第一个共享(reader)锁定会阻塞第二个线程。 416 | 417 | SRW 锁与条件变量一起引入了 Windows API。`CONDITION_VARIABLE` 仅占用一个指针的大小,可以使用 `CONDITION_VARIABLE_INIT` 进行静态初始化,不需要销毁。只要它没有被使用(被借用),我们也可以移动它。 418 | 419 | 条件变量不仅通过 SleepConditionVariableSRW 与 SRW 锁一起使用,还可以通过 SleepConditionVariableCS 与临界区一起使用。 420 | 421 | 唤醒等待线程要么通过 WakeConditionVariable 唤醒单个线程,要么通过 WakeAllConditionVariable 唤醒所有等待线程。 422 | 423 | > 最初,标准库中使用的 Windows SRW 锁和条件变量被包装在 Box 中,以避免移动对象。直到我们在 2020 年要求之后,微软才记录了这些对象的可移动性保证。自 Rust 1.49 起,`std::sync::Mutex`、`std::sync::RwLock` 和 `std::sync::Condvar` 在 Windows Vista 及更高版本中直接封装了 SRWLOCK 或 CONDITION_VARIABLE,无需任何额外的内存分配。 424 | 425 | ### 基于地址的等待 426 | 427 | (英文版本) 428 | 429 | Windows 8(和 Windows Server 2012)引入了一种新的、更灵活的同步功能类型,非常类似于本章前面讨论的 Linux `FUTEX_WAIT` 和 `FUTEX_WAKE` 操作。 430 | 431 | `WaitOnAddress` 函数可以操作 8 位、16 位、32 位 或 64 位的原子变量。它接收了 4 个参数:原子变量地址、保存期望值的变量地址、原子变量大小(以字节为单位)以及在放弃之前的最大等待最大毫秒数(或者无限超时的 `u32::MAX`)。 432 | 433 | 就像 FUTEX_WAIT 操作一样,它将原子变量的值与预期值进行比较,如果匹配则进入睡眠状态,等待相应的唤醒操作。检查和睡眠操作相对于唤醒操作是原子发生的,这意味着没有唤醒信号会在两者之间丢失。 434 | 435 | 唤醒正在等待 `WaitOnAddress` 的线程可以通过 `WakeByAddressSingle` 来唤醒单个线程,或者通过 `WakeByAddressAll` 来唤醒所有等待的线程。这两个函数只接受一个参数:原子变量的地址,该地址也被传递给 `WaitOnAddress`。 436 | 437 | Windows API 的一些(但不是全部)同步原语是使用这些函数实现的。更重要的是,它们是构建我们自己的原始物的绝佳基石,我们将在[第九章](./9_Building_Our_Own_Locks.md)中这样做。 438 | 439 | ## 总结 440 | 441 | (英文版本) 442 | 443 | * *系统调用*(syscall)是进入操作系统内核的调用,与普通函数调用相比,相对较慢。 444 | * 通常,程序不直接进行系统调用,而是通过操作系统的库(如 `libc`)与内核进行交互。在许多操作系统中,这是与内核进行交互的唯一支持方式。 445 | * libc crate 提供了 Rust 代码访问 libc 的能力。 446 | * 在 POSIX 系统上,libc 包含了不仅符合 C 标准所需的内容,还符合 POSIX 标准的内容。 447 | * POSIX 标准包括 *pthread*,这是一个具有并发原语(如 `pthread_mutex_t`)的库。 448 | * pthread 类型是为 C 设计的,而不是为 Rust 设计的。例如,它们不可移动,这可能是一个问题。 449 | * Linux 有一个 *futex* 系统调用,支持在 AtomicU32 上进行几种等待和唤醒操作。等待操作验证原子的期望值,以避免错过通知。 450 | * 除了 pthread,macOS 还提供了 `os_unfair_lock` 作为轻量级锁定原语。 451 | * Windows 的重量级并发原语始终需要与内核进行交互,但可以在进程之间传递,并与标准的 Windows 等待函数一起使用。 452 | * Windows 的轻量级并发原语包括“slim”读写锁(SRW 锁)和条件变量。这些可以很容易地在 Rust 中包装,因为它们是可移动的。 453 | * Windows 还通过 WaitOnAddress 和 WakeByAddress 提供了类似 futex 的基本功能。 454 | 455 |

456 | 下一篇,第九章:构建我们自己的「锁」 457 |

458 | 459 | [^1]: 460 | [^2]: 绝对时间。表示系统(或程序)启动后流逝的时间,更改系统的时间对它没有影响。每次系统(或程序)启动时,该值都归 0 461 | [^3]: 挂钟时间,即现实世界里我们感知到的时间,如 2008-08-08 20:08:00。但对计算机而言,这个时间不一定是单调递增的。因为人觉得当前机器的时间不准,可以随意拨慢或调快。 462 | [^4]: 463 | [^5]: 464 | [^6]: 465 | [^7]: 466 | [^8]: 467 | 468 | 参考: 469 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 rustcc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [第一章:Rust 并发基础](./1_Basic_of_Rust_Concurrency.md) 2 | 3 | * [Rust 中的线程](./1_Basic_of_Rust_Concurrency.md#rust-中的线程) 4 | * [作用域内的线程](./1_Basic_of_Rust_Concurrency.md#作用域内的线程) 5 | * [共享所有权以及引用计数](./1_Basic_of_Rust_Concurrency.md#共享所有权以及引用计数) 6 | * [静态值(static)](./1_Basic_of_Rust_Concurrency.md#静态值static) 7 | * [泄漏(Leak)](./1_Basic_of_Rust_Concurrency.md#泄漏leak) 8 | * [引用计数](./1_Basic_of_Rust_Concurrency.md#引用计数) 9 | * [借用和数据竞争](./1_Basic_of_Rust_Concurrency.md#借用和数据竞争) 10 | * [内部可变性](./1_Basic_of_Rust_Concurrency.md#内部可变性)([Cell](./1_Basic_of_Rust_Concurrency.md#cell)、[RefCell](./1_Basic_of_Rust_Concurrency.md#refcell)、[互斥锁以及读写锁](./1_Basic_of_Rust_Concurrency.md#互斥锁和读写锁)、[Atomic](./1_Basic_of_Rust_Concurrency.md#atomic)、[UnsafeCell](./1_Basic_of_Rust_Concurrency.md#unsafecell)) 11 | * [线程安全:Send 和 Sync](./1_Basic_of_Rust_Concurrency.md#线程安全send-和-sync) 12 | * [锁:互斥锁和读写锁](./1_Basic_of_Rust_Concurrency.md#锁互斥锁和读写锁) 13 | * [Rust 的互斥锁](./1_Basic_of_Rust_Concurrency.md#rust-的互斥锁) 14 | * [锁中毒](./1_Basic_of_Rust_Concurrency.md#锁中毒poison) 15 | * [读写锁](./1_Basic_of_Rust_Concurrency.md#读写锁) 16 | * [等待:阻塞和条件变量](./1_Basic_of_Rust_Concurrency.md#等待-阻塞park和条件变量) 17 | * [线程阻塞](./1_Basic_of_Rust_Concurrency.md#线程阻塞) 18 | * [条件变量](./1_Basic_of_Rust_Concurrency.md#条件变量) 19 | * [总结](./1_Basic_of_Rust_Concurrency.md#总结) 20 | 21 | ## [第二章:Atomic](./2_Atomics.md) 22 | 23 | * [Atomic 的加载和存储操作](./2_Atomics.md#atomic-的加载和存储操作) 24 | * [示例:停止标识](./2_Atomics.md#示例停止标识) 25 | * [示例:进度报道](./2_Atomics.md#示例进度报道) 26 | * [同步](./2_Atomics.md#同步) 27 | * [示例:惰性初始化](./2_Atomics.md#示例惰性初始化) 28 | * [获取并修改操作](./2_Atomics.md#获取并修改操作) 29 | * [示例:来自多线程的进度报道](./2_Atomics.md#示例来自多线程的进度报道) 30 | * [示例:统计数据](./2_Atomics.md#示例统计数据) 31 | * [示例:ID 分配](./2_Atomics.md#示例id-分配) 32 | * [比较并交互操作](./2_Atomics.md#比较并交换操作) 33 | * [示例:没有溢出的 ID 分配](./2_Atomics.md#示例没有溢出的-id-分配) 34 | * [示例:惰性一次性初始化](./2_Atomics.md#示例惰性一次性初始化) 35 | * [总结](./2_Atomics.md#总结) 36 | 37 | ## [第三章:内存排序](./3_Memory_Ordering.md) 38 | 39 | * [重排和优化](./3_Memory_Ordering.md#重排和优化) 40 | * [内存模型](./3_Memory_Ordering.md#内存模型) 41 | * [Happens-Before 关系](./3_Memory_Ordering.md#happens-before-关系) 42 | * [spawn 和 join](./3_Memory_Ordering.md#spawn-和-join) 43 | * [Relaxed 排序](./3_Memory_Ordering.md#relaxed-排序) 44 | * [Release 和 Acquire 排序](./3_Memory_Ordering.md#release-和-acquire-排序) 45 | * [示例:锁定](./3_Memory_Ordering.md#示例锁定) 46 | * [示例:使用间接的方式惰性初始化](./3_Memory_Ordering.md#示例使用间接的方式惰性初始化) 47 | * [Consume 排序](./3_Memory_Ordering.md#consume-排序) 48 | * [顺序一致性排序](./3_Memory_Ordering.md#顺序一致性排序) 49 | * [屏障(Fence)](./3_Memory_Ordering.md#屏障fence2) 50 | * [常见的误解](./3_Memory_Ordering.md#常见的误解) 51 | * [总结](./3_Memory_Ordering.md#总结) 52 | 53 | ## [第四章:构建我们自己的自旋锁](./4_Building_Our_Own_Spin_Lock.md) 54 | 55 | * [一个最小实现](./4_Building_Our_Own_Spin_Lock.md#一个最小实现) 56 | * [一个不安全的自旋锁](./4_Building_Our_Own_Spin_Lock.md#一个不安全的自旋锁) 57 | * [使用锁守卫的安全接口](./4_Building_Our_Own_Spin_Lock.md#使用锁守卫的安全接口) 58 | * [总结](./4_Building_Our_Own_Spin_Lock.md#总结) 59 | 60 | ## [第五章:构建我们自己的 Channel](./5_Building_Our_Own_Channels.md) 61 | 62 | * [一个简单的以 mutex 为基础的 Channel](./5_Building_Our_Own_Channels.md#一个简单的以-mutex-为基础的-channel) 63 | * [一个不安全的一次性 Channel](./5_Building_Our_Own_Channels.md#一个不安全的一次性-channel) 64 | * [通过运行时检查来达到安全](./5_Building_Our_Own_Channels.md#通过运行时检查来达到安全) 65 | * [通过类型来达到安全](./5_Building_Our_Own_Channels.md#通过类型来达到安全) 66 | * [借用以避免内存分配](./5_Building_Our_Own_Channels.md#借用以避免内存分配) 67 | * [阻塞](./5_Building_Our_Own_Channels.md#阻塞) 68 | * [总结](./5_Building_Our_Own_Channels.md#总结) 69 | 70 | ## [第六章:构建我们自己的“Arc”](./6_Building_Our_Own_Arc.md) 71 | 72 | * [基础的引用计数](./6_Building_Our_Own_Arc.md#基础的引用计数) 73 | * [测试它](./6_Building_Our_Own_Arc.md#测试它) 74 | * [可变性](./6_Building_Our_Own_Arc.md#可变性) 75 | * [Weak 指针](./6_Building_Our_Own_Arc.md#weak-指针) 76 | * [测试它](./6_Building_Our_Own_Arc.md#测试它2) 77 | * [优化](./6_Building_Our_Own_Arc.md#优化) 78 | * [总结](./6_Building_Our_Own_Arc.md#总结) 79 | 80 | ## [第七章:理解处理器](./7_Understanding_the_Processor.md) 81 | 82 | * [处理器指令](./7_Understanding_the_Processor.md#处理器指令) 83 | * [加载和存储操作](./7_Understanding_the_Processor.md#加载和存储操作) 84 | * [读并修改并写操作](./7_Understanding_the_Processor.md#读并修改并写操作) 85 | * [x86 lock 前缀](./7_Understanding_the_Processor.md#x86-lock-前缀) 86 | * [x86 比较并交换指令](./7_Understanding_the_Processor.md#x86-比较并交换指令) 87 | * [ll-和-sc-指令](./7_Understanding_the_Processor.md#ll-和-sc-指令) 88 | * [arm-的-ldxr-和-stxr-指令](./7_Understanding_the_Processor.md#arm-的-ldxr-和-stxr-指令) 89 | * [arm-的比较并交换操作](./7_Understanding_the_Processor.md#arm-的比较并交换操作) 90 | * [缓存](./7_Understanding_the_Processor.md#缓存3) 91 | * [缓存一致性](./7_Understanding_the_Processor.md#缓存一致性) 92 | * [write-through 协议](./7_Understanding_the_Processor.md#write-through-协议) 93 | * [MESI 协议](./7_Understanding_the_Processor.md#mesi-协议4) 94 | * [对性能的影响](./7_Understanding_the_Processor.md#对性能的影响) 95 | * [重排](./7_Understanding_the_Processor.md#重排) 96 | * [内存排序](./7_Understanding_the_Processor.md#内存排序) 97 | * [x86-64:强排序](./7_Understanding_the_Processor.md#x86-64强排序) 98 | * [ARM64:弱排序](./7_Understanding_the_Processor.md#arm64弱排序) 99 | * [一个实验](./7_Understanding_the_Processor.md#一个实验) 100 | * [内存屏障](./7_Understanding_the_Processor.md#内存屏障) 101 | * [总结](./7_Understanding_the_Processor.md#总结) 102 | 103 | ## [第八章:操作系统原语](./8_Operating_System_Primitives.md) 104 | 105 | * [使用内核接口](./8_Operating_System_Primitives.md#使用内核接口) 106 | * [POSIX](./8_Operating_System_Primitives.md#posix) 107 | * [在 Rust 中包装类型](./8_Operating_System_Primitives.md#在-rust-中包装类型) 108 | * [Linux](./8_Operating_System_Primitives.md#linux) 109 | * [Futex](./8_Operating_System_Primitives.md#futex) 110 | * [Futex 操作](./8_Operating_System_Primitives.md#futex-操作) 111 | * [优先继承 Futex 操作](./8_Operating_System_Primitives.md#优先继承-futex-操作) 112 | * [macOS](./8_Operating_System_Primitives.md#macos) 113 | * [os_unfair_lock](./8_Operating_System_Primitives.md#os_unfair_lock) 114 | * [Windows](./8_Operating_System_Primitives.md#windows) 115 | * [重量级内核对象](./8_Operating_System_Primitives.md#重量级内核对象) 116 | * [轻量级对象](./8_Operating_System_Primitives.md#轻量级对象) 117 | * [精简的读写锁(SRW)](./8_Operating_System_Primitives.md#精简的读写srw锁5) 118 | * [基于地址的等待](./8_Operating_System_Primitives.md#基于地址的等待) 119 | * [总结](./8_Operating_System_Primitives.md#总结) 120 | 121 | ## [第九章:构建我们自己的「锁」](./9_Building_Our_Own_Locks.md) 122 | 123 | * [Mutex](./9_Building_Our_Own_Locks.md#mutex) 124 | * [避免系统调用](./9_Building_Our_Own_Locks.md#避免系统调用) 125 | * [进一步优化](./9_Building_Our_Own_Locks.md#进一步优化) 126 | * [基准测试](./9_Building_Our_Own_Locks.md#基准测试) 127 | * [条件变量](./9_Building_Our_Own_Locks.md#条件变量) 128 | * [避免系统调用2](./9_Building_Our_Own_Locks.md#避免系统调用2) 129 | * [避免虚假唤醒](./9_Building_Our_Own_Locks.md#避免虚假唤醒) 130 | * [读写锁](./9_Building_Our_Own_Locks.md#读写锁) 131 | * [避免 writer 忙碌循环](./9_Building_Our_Own_Locks.md#避免-writer-忙碌循环) 132 | * [避免 writer 陷入饥饿](./9_Building_Our_Own_Locks.md#避免-writer-陷入饥饿) 133 | * [总结](./9_Building_Our_Own_Locks.md#总结) 134 | 135 | ## [第十章:理念和灵感](./10_Ideas_and_Inspiration.md) 136 | 137 | * [信号量](./10_Ideas_and_Inspiration.md#信号) 138 | * [RCU](./10_Ideas_and_Inspiration.md#rcu) 139 | * [无锁链表](./10_Ideas_and_Inspiration.md#无锁链表) 140 | * [基于队列的锁](./10_Ideas_and_Inspiration.md#基于队列的锁) 141 | * [基于阻塞的锁](./10_Ideas_and_Inspiration.md#基于阻塞的锁) 142 | * [顺序锁](./10_Ideas_and_Inspiration.md#顺序锁sequence-lock) 143 | * [教学材料](./10_Ideas_and_Inspiration.md#教学材料) 144 | 145 | ## [索引](./attachment.md) 146 | 147 | 注明:本文译自 ,若其它平台引用此翻译,也请注明出处。 148 | 149 | 加入我们,编写[索引](https://github.com/rustcc/Rust_Atomics_and_Locks/blob/main/attachment.md#索引)或者进行[校对](https://github.com/rustcc/Rust_Atomics_and_Locks/issues/28)以帮助更多的人 150 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | :root{ 7 | --light-color-theme: rgb(101, 123, 131); 8 | --light-bg-theme: #fff; 9 | --light-color-a: #6ca586; 10 | --light-color-a-hover: #046960; 11 | --light-bg-code: rgba(69, 132, 161, 0.85); 12 | --light-bg-pre: rgb(254, 246, 229); 13 | --light-color-h: #402e52; 14 | 15 | --dark-color-theme: var(--light-bg-theme); 16 | --dark-bg-theme: #000; 17 | --dark-color-a: rgba(96, 255, 220, 0.7); 18 | --dark-color-a-hover: #04fbc5; 19 | --dark-bg-code: rgb(31, 91, 62); 20 | --dark-border-table: var(--dark-bg-code); 21 | --dark-bg-pre: rgb(45, 43, 85); 22 | --dark-color-h: rgb(0, 208, 212); 23 | } 24 | 25 | html,body, .markdown-body { 26 | color: var(--light-color-theme); 27 | } 28 | 29 | a { 30 | color: var(--light-color-a); 31 | } 32 | 33 | a:hover { 34 | text-decoration: none; 35 | color: var(--light-color-a-hover); 36 | } 37 | 38 | .markdown-body code { 39 | background-color: var(--light-bg-code); 40 | color: white; 41 | } 42 | 43 | .markdown-body .highlight, 44 | .markdown-body pre { 45 | background: var(--light-bg-pre); 46 | pre:where(.highlight){ 47 | background: var(--light-bg-pre); 48 | } 49 | 50 | code.language-txt { 51 | color: var(--light-color-theme); 52 | } 53 | 54 | .c { 55 | color: rgb(147, 161, 161); 56 | } 57 | 58 | .n{ 59 | color:rgb(101, 123, 131); 60 | } 61 | 62 | .k, .kv, .o { 63 | color: rgb(133, 153, 0); 64 | } 65 | 66 | .nd, .nn, .nf, .nb, .py { 67 | color: rgb(38, 139, 210); 68 | } 69 | 70 | .p { 71 | color: rgb(255, 215, 0); 72 | } 73 | 74 | .mi { 75 | color: rgb(42, 161, 152) 76 | } 77 | } 78 | 79 | h1, 80 | h2, 81 | h3, 82 | h4, 83 | h5, 84 | h6 { 85 | color: var(--light-color-h); 86 | } 87 | 88 | .box { 89 | border: medium solid green; 90 | padding: 1rem; 91 | pre { 92 | color: black; 93 | } 94 | } 95 | 96 | @media (prefers-color-scheme: dark) { 97 | html,body,.markdown-body { 98 | background-color: var(--dark-bg-theme); 99 | color: var(--dark-color-theme); 100 | } 101 | 102 | .markdown-body table th, 103 | .markdown-body table td{ 104 | border-color: var(--dark-border-table); 105 | background-color: var(--dark-bg-theme); 106 | } 107 | 108 | .markdown-body .highlight, 109 | .markdown-body pre { 110 | background: var(--dark-bg-pre); 111 | 112 | pre:where(.highlight){ 113 | background: var(--dark-bg-pre); 114 | } 115 | 116 | code.language-txt { 117 | color: var(--dark-color-theme); 118 | } 119 | 120 | .c { 121 | color: rgb(179, 98, 255); 122 | } 123 | 124 | .n { 125 | color: #fff; 126 | } 127 | 128 | .k, .kv, .o { 129 | color: rgb(255, 157, 0); 130 | } 131 | 132 | .nd, .nn, .nf, .nb { 133 | color: rgb(250, 208, 0); 134 | } 135 | 136 | .p { 137 | color: rgb(222, 236, 252); 138 | } 139 | 140 | .mi { 141 | color: rgb(236,92,133) 142 | } 143 | } 144 | 145 | .box pre { 146 | color: var(--dark-color-theme); 147 | } 148 | 149 | .markdown-body code { 150 | background: var(--dark-bg-code); 151 | } 152 | 153 | h1, 154 | h2, 155 | h3, 156 | h4, 157 | h5, 158 | h6 { 159 | color: var(--dark-color-h); 160 | } 161 | 162 | a { 163 | color: var(--dark-color-a); 164 | } 165 | 166 | a:hover { 167 | color: var(--dark-color-a-hover); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /attachment.md: -------------------------------------------------------------------------------- 1 | ## 索引 2 | 3 | ### A 4 | 5 | * AArch64(参见 ARM64) 6 | * ABA 问题,[#](./2_Atomics.md#index-ABAproblem) 7 | * 终止进程,[#](./2_Atomics.md#index-abortingtheprocess) 8 | * AcqRel,[#](./3_Memory_Ordering.md#index-AcqRel) 9 | * (参见 release 和 acquire 内存排序) 10 | * acquire 内存排序(参见 release 和 acquire 内存排序) 11 | * add 指令(ARM),[#](./7_Understanding_the_Processor.md#index-addinstructionARM) 12 | * add 指令(x86),[#](./7_Understanding_the_Processor.md#index-addinstructionx86) 13 | * 基于地址的等待(Windows),[#](./8_Operating_System_Primitives.md#基于地址的等待) 14 | * (参见 futex) 15 | * 凭空出现的值,[#](./3_Memory_Ordering.md#凭空出现的值) 16 | * alignment,[#](./7_Understanding_the_Processor.md#index-alignment) 17 | * 分配(参见 ID 分配) 18 | * AMD 处理器,[#](./7_Understanding_the_Processor.md#index-AMDprocessors) 19 | * and 指令(x86),[#](./7_Understanding_the_Processor.md#index-x86-64instructions-and) 20 | * Arc,[#](./1_Basic_of_Rust_Concurrency.md#index-Arc) 21 | * 构建我们自己的 Arc,[#](./6_Building_Our_Own_Arc.md#index-buildingourown-Arc) 22 | * 循环结构,[#](./6_Building_Our_Own_Arc.md#index-Arc-cyclicstructures) 23 | * get_mut,[#](./6_Building_Our_Own_Arc.md#index-Arc-getmut) 24 | * 内存排序,[#](./6_Building_Our_Own_Arc.md#index-Arc-memoryordering),[#](./6_Building_Our_Own_Arc.md#index-happens-beforerelationships-inArc-2),[#](./6_Building_Our_Own_Arc.md#index-Arc-memoryordering-3),[#](./6_Building_Our_Own_Arc.md#index-Arc-memoryordering-4) 25 | * 命名克隆,[#](./1_Basic_of_Rust_Concurrency.md#命名克隆) 26 | * 用于 Channel 的情况,[#](./5_Building_Our_Own_Channels.md#index-Arc-usingforchannelstate) 27 | * weak 指针,[#](./6_Building_Our_Own_Arc.md#weak-指针) 28 | * 性能开销,[#](./6_Building_Our_Own_Arc.md#index-Arc-weakpointers-performancecost) 29 | * arguments,consuming,[#](./5_Building_Our_Own_Channels.md#index-argumentsconsuming) 30 | * ARM64(处理器架构),[#](./7_Understanding_the_Processor.md#index-ARM64processorarchitecture) 31 | * aarch64-unknown-linux-musl target,[#](./7_Understanding_the_Processor.md#index-ARM64processorarchitecture-aarch64-unknown-linux-musltarget) 32 | * other-multi-copy atomic,[#](./7_Understanding_the_Processor.md#index-ARM64processorarchitecture-other-multi-copyatomic) 33 | * weakly ordered,[#](./7_Understanding_the_Processor.md#arm64弱排序) 34 | * ARM64 指令 35 | * add,# 36 | * ARMv8.1 atomic instructions,#,# 37 | * b.ne (branch if not equal),# 38 | * cbnz (compare and branch on nonzero),# 39 | * clrex (clear exclusive),# 40 | * cmp (compare),# 41 | * dmb (data memory barrier),# 42 | * ldar (load-acquire register),# 43 | * ldaxr (load-acquire exclusive register),# 44 | * ldr (load register),# 45 | * ldxr (load exclusive register),# 46 | * load-linked and store-conditional instructions,# 47 | * mov (move),# 48 | * overview,# 49 | * ret (return),# 50 | * stlr (store-release register),# 51 | * stlxr (store-release exclusive register),# 52 | * str (store register),# 53 | * stxr (store exclusive register),# 54 | * ARMv8 (see ARM64) 55 | * ARMv8.1 atomic instructions,#,# 56 | * overview,# 57 | * array::from_fn,# 58 | * assembler,# 59 | * assembly,# 60 | * inspecting compiler output,# 61 | * atomic,#,# 62 | * compare-and-exchange operations,# 63 | * weak,#,#,#,#,#,#,#,#,#,#,#,# 64 | * fetch-and-modify operations,# 65 | * wrapping behavior (add and sub),#,#,#,# 66 | * load and store operations,# 67 | * example,stop flag,#,#,#,#,# 68 | * memory ordering (see memory ordering) 69 | * reference counting (see Arc) 70 | * atomic barriers (see fences) 71 | * atomic fences (see fences) 72 | * atomic types,#,# 73 | * compare_exchange,# 74 | * compare_exchange_weak,# 75 | * fetch_add,# 76 | * wrapping behavior,# 77 | * (see also overflows) 78 | * fetch_and,# 79 | * fetch_max,# 80 | * fetch_min,# 81 | * fetch_nand,# 82 | * fetch_or,# 83 | * fetch_store (see swap) 84 | * fetch_sub,# 85 | * wrapping behavior,# 86 | * (see also overflows) 87 | * fetch_update,# 88 | * fetch_xor,# 89 | * get_mut,# 90 | * load,# 91 | * store,# 92 | * swap,# 93 | * atomic-wait crate,# 94 | * AtomicBool,# 95 | * (see also atomic types) 96 | * locking using,#,# 97 | * AtomicI8 (see atomic types) 98 | * AtomicI16 (see atomic types) 99 | * AtomicI32 (see atomic types) 100 | * AtomicI64 (see atomic types) 101 | * AtomicIsize (see atomic types) 102 | * AtomicPtr,# 103 | * (see also atomic types) 104 | * compare-and-exchange,# 105 | * lazy initialization,# 106 | * AtomicU8 (see atomic types) 107 | * AtomicU16 (see atomic types) 108 | * AtomicU32 (see atomic types) 109 | * AtomicU64 (see atomic types) 110 | * AtomicUsize (see atomic types) 111 | * auto traits,# 112 | 113 | ### B 114 | 115 | * b.ne (branch if not equal) instruction (ARM),# 116 | * barriers (see fences) 117 | * basics,# 118 | * benchmarking,#,# 119 | * black_box,avoiding optimizations with,#,# 120 | * binary semaphore,# 121 | * black_box,#,# 122 | * blocking,# 123 | * channel,# 124 | * condition variables,# 125 | * futex wait operation,# 126 | * (see also futex) 127 | * mutexes,# 128 | * Once and OnceLock,#,# 129 | * semaphores,# 130 | * spin loop,# 131 | * thread parking (see thread parking) 132 | * boolean (atomic) (see AtomicBool) 133 | * borrowing,# 134 | * bending the rules,# 135 | * error,# 136 | * exclusive,# 137 | * from multiple threads (Sync),# 138 | * immutable,# 139 | * (see also shared) 140 | * local variables in a thread,# 141 | * mutable,# 142 | * (see also exclusive) 143 | * shared,# 144 | * splitting,# 145 | * undefined behavior,# 146 | * Box 147 | * from_raw,#,# 148 | * into_raw,# 149 | * leak,#,# 150 | * unmovable type,wrapping in,# 151 | * btc (bit test and complement) instruction (x86),# 152 | * btr (bit test and reset) instruction (x86),# 153 | * bts (bit test and set) instruction (x86),# 154 | * building our own 155 | * Arc,# 156 | * channels,# 157 | * condition variables,# 158 | * mutexes,# 159 | * reader-writer locks,# 160 | * spin locks,# 161 | * busy-looping,# 162 | * (see also spinning) 163 | 164 | ### C 165 | 166 | * C standard library,# 167 | * (see also libc) 168 | * cache coherence,# 169 | * protocol,# 170 | * write-through,#,#,#,# 171 | * cache lines,# 172 | * performance experiment,# 173 | * cache miss,# 174 | * caching (processors),# 175 | * (see also cache coherence) 176 | * compare-and-exchange operations,effect of,# 177 | * per core,# 178 | * performance experiment,# 179 | * cargo-show-asm,# 180 | * cas (compare and swap) instruction (ARM),# 181 | * casa (compare and swap,acquire) instruction (ARM),# 182 | * casal (compare and swap,acquire and release) instruction (ARM),# 183 | * casl (compare and swap,release) instruction (ARM),# 184 | * cbnz (compare and branch on nonzero) instruction (ARM),# 185 | * Cell,# 186 | * unsafe (see UnsafeCell) 187 | * channels 188 | * blocking,# 189 | * borrowing,# 190 | * building our own,# 191 | * dropping,# 192 | * one-shot,# 193 | * safe interface,# 194 | * Sender and Receiver types,#,# 195 | * storing in Arc,# 196 | * avoiding,# 197 | * unsafe interface,# 198 | * Clone trait,#,#,#,#,# 199 | * closures 200 | * captured values 201 | * moving,#,# 202 | * spawning scoped threads using,# 203 | * spawning threads using,# 204 | * clrex (clear exclusive) instruction (ARM),# 205 | * cmp (compare) instruction (ARM),# 206 | * cmpxchg (compare and exchange) instruction (x86),# 207 | * `#[cold]`,# 208 | * compare-and-exchange operations (atomic),# 209 | * on ARM64,# 210 | * caching,effect on,# 211 | * compiler optimization,# 212 | * example,ID allocation,# 213 | * example,lazy initialization,#,# 214 | * memory ordering,# 215 | * using for channel state,# 216 | * using for mutex state,# 217 | * using for reader-writer lock state,# 218 | * using on AtomicPtr,# 219 | * using to lock reference counter,# 220 | * weak,# 221 | * on ARM64,# 222 | * on x86-64,# 223 | * Compiler Explorer,# 224 | * compiler fence,#,# 225 | * compiler optimization 226 | * black_box,avoiding with,#,# 227 | * `#[cold]`,# 228 | * of compare-and-exchange loops,# 229 | * enabling,#,# 230 | * `#[inline]` # 231 | * reordering,# 232 | * complex instruction set computer (CISC),# 233 | * concurrency,basics,# 234 | * condition variables,# 235 | * building our own,# 236 | * example,# 237 | * memory ordering,# 238 | * pthread_cond_t,# 239 | * thundering herd problem,# 240 | * timeout,# 241 | * using to build a channel,# 242 | * Windows,# 243 | * Condvar,# 244 | * (see also condition variables) 245 | * consume memory ordering,# 246 | * consuming arguments by value,# 247 | * contention (mutexes),#,# 248 | * benchmarking,# 249 | * Copy trait,#,# 250 | * atomic types,not implementing,# 251 | * moving,# 252 | * critical section (Windows),# 253 | * current thread,#,#,# 254 | * cyclic structures (Arc),# 255 | 256 | ### D 257 | 258 | * data races,# 259 | * avoiding using atomics,#,# 260 | * Deref trait,#,#,# 261 | * DerefMut trait,#,#,#,# 262 | * disassembler,#,# 263 | * dmb (data memory barrier) instruction (ARM),# 264 | * drop function,#,#,# 265 | * Drop trait,#,#,#,#,#,#,#,#,#,#,#,# 266 | * dword,# 267 | 268 | ### E 269 | 270 | * --emit=asm (rustc),# 271 | * exclusive references,# 272 | 273 | ### F 274 | 275 | * fair locks,# 276 | * false sharing,# 277 | * fences,#,#,# 278 | * on ARM64,# 279 | * compiler fence,#,# 280 | * instructions,# 281 | * process-wide memory barriers,# 282 | * on x86-64,# 283 | * fetch-and-modify operations (atomic),# 284 | * on ARM64,# 285 | * example,ID allocation,# 286 | * example,progress reporting,# 287 | * example,statistics,# 288 | * wrapping behavior (add and sub),# 289 | * (see also overflows) 290 | * on x86-64,# 291 | * fetch_store operation (atomic) (see swap operation) 292 | * fetch_update (atomic),# 293 | * FlushProcessWriteBuffers (Windows),# 294 | * forgetting (see leaking) 295 | * FreeBSD,umtx_op syscall,# 296 | * (see also futex) 297 | * from_fn (array),# 298 | * futex,# 299 | * cross-platform futex-like functionality,# 300 | * example,# 301 | * memory safety,# 302 | * on other platforms,# 303 | * operations (Linux),# 304 | * FUTEX_WAIT,#,#,#,#,#,#,#,#,#,#,# 305 | * requeuing,#,# 306 | * spurious wake-ups,# 307 | * timeout,#,# 308 | * wait operation,# 309 | * wake operation,# 310 | 311 | ### G 312 | 313 | * globally consistent order,# 314 | * (see also sequentially consistent memory ordering) 315 | * Godbolt,# 316 | * good luck,# 317 | * guards 318 | * dropping,# 319 | * join guard,# 320 | * mutex guard,#,# 321 | * read guard,#,# 322 | * spin lock guard,# 323 | * write guard,#,# 324 | 325 | ### H 326 | 327 | * hand,things getting out of,# 328 | * happens-before relationships,# 329 | * in Arc,#,# 330 | * between threads,# 331 | * locking and unlocking,#,# 332 | * spawning and joining threads,# 333 | * through a release-acquire pair,#,# 334 | * chaining,# 335 | * within the same thread,# 336 | * hint::black_box,#,# 337 | * hint::spin_loop,# 338 | 339 | ### I 340 | 341 | * ID allocation 342 | * using compare_exchange_weak,# 343 | * using fetch_add,# 344 | * using fetch_update,# 345 | * ideas and inspiration,# 346 | * if let statement 347 | * lifetime of temporaries,# 348 | * ignorance,blissful,# 349 | * immutable references,# 350 | * (see also shared references) 351 | * indivisible,# 352 | * `#[inline]`,# 353 | * inspiration,# 354 | * Instant,# 355 | * instruction reordering (see reordering) 356 | * instructions,# 357 | * (see also ARM64 instructions; x86-64 instructions) 358 | * compare-and-exchange operations,#,# 359 | * fences,# 360 | * load and store operations,# 361 | * load-linked/store-conditional (LL/SC) instructions,# 362 | * memory ordering,# 363 | * overview,# 364 | * read-modify-write operations,# 365 | * Intel processors,# 366 | * interior mutability,#,#,# 367 | * invalidation queues,# 368 | 369 | ### J 370 | 371 | * jne (jump if not equal) instruction (x86),# 372 | * join method,# 373 | * JoinGuard,# 374 | * JoinHandle,# 375 | * joining threads,# 376 | * happens-before relationship,# 377 | 378 | ### K 379 | 380 | * kernel,#,# 381 | * interfacing with,# 382 | * kernel-managed objects (Windows),# 383 | 384 | ### L 385 | 386 | * L1/L2/L3/L4 cache,# 387 | * label (assembly),# 388 | * lazy initialization 389 | * using compare_exchange,# 390 | * using compare_exchange and allocation,# 391 | * using load and store,# 392 | * ldadd (load and add) instruction (ARM),# 393 | * ldadda (load and add,acquire) instruction (ARM),# 394 | * ldaddal (load and add,acquire and release) instruction (ARM),# 395 | * ldaddl (load and add,release) instruction (ARM),# 396 | * ldar (load-acquire register) instruction (ARM),# 397 | * ldaxr (load-acquire exclusive register) instruction (ARM),# 398 | * ldr (load register) instruction (ARM),# 399 | * ldxr (load exclusive register) instruction (ARM),# 400 | * leaking,#,# 401 | * by mistake,# 402 | * a MutexGuard,# 403 | * “Leakpocalypse”,# 404 | * libc,# 405 | * pthreads functionality in,# 406 | * libpthread,# 407 | * (see also pthreads) 408 | * lifetime 409 | * elision,#,# 410 | * in a struct,# 411 | * of mutex guard,# 412 | * specifying using plain English,# 413 | * static,# 414 | * linked list,# 415 | * Linux 416 | * futex syscall,# 417 | * (see also futex) 418 | * arguments,# 419 | * futex_waitv syscall,# 420 | * interfacing with the kernel,# 421 | * libc,role of,# 422 | * membarrier syscall,# 423 | * process-wide memory barrier,# 424 | * RCU,# 425 | * load and store operations (atomic),# 426 | * on ARM64 and x86-64,# 427 | * compared to non-atomic operations,#,# 428 | * example,lazy initialization,# 429 | * example,progress reporting,# 430 | * example,stop flag,# 431 | * load-linked/store-conditional (LL/SC) loop,# 432 | * on ARM64,# 433 | * compiler optimization,# 434 | * lock poisoning,# 435 | * lock prefix (x86),# 436 | * lock_api crate,# 437 | * luck,good,# 438 | 439 | ### M 440 | 441 | * machine code,# 442 | * machine instructions (see instructions) 443 | * macOS 444 | * futex-like functionality on,# 445 | * interfacing with the kernel,#,# 446 | * os_unfair_lock,# 447 | * main thread,# 448 | * ManuallyDrop,# 449 | * MaybeUninit,#,#,# 450 | * mem::forget,# 451 | * membarrier syscall,# 452 | * memory barriers (see fences) 453 | * memory fences (see fences) 454 | * memory model,# 455 | * memory ordering,#,# 456 | * on ARM64,# 457 | * compiler fence,#,# 458 | * consume,# 459 | * experiment,using relaxed instead of release and acquire,# 460 | * fences,#,#,# 461 | * happens-before relationship,#,# 462 | * Miri,detecting problems with,# 463 | * misconceptons about,# 464 | * out-of-thin-air values,# 465 | * at processor level,# 466 | * reference counting,#,#,#,#,# 467 | * relaxed,#,# 468 | * release and acquire,# 469 | * (see also release and acquire memory ordering) 470 | * locking and unlocking,# 471 | * sequentially consistent,# 472 | * (see also sequentially consistent memory ordering) 473 | * specifying using plain English,# 474 | * total modification order,#,#,#,# 475 | * on x86-64,# 476 | * MESI cache coherence protocol,# 477 | * MESIF cache coherence protocol,# 478 | * mfence (memory fence) instruction (x86),# 479 | * microinstructions,# 480 | * Miri,# 481 | * MOESI cache coherence protocol,# 482 | * mov (move) instruction (ARM),# 483 | * mov (move) instruction (x86),# 484 | * movable,not 485 | * critical section (Windows),# 486 | * Pin,# 487 | * pthread types,# 488 | * wrapping in Box,# 489 | * move closure,# 490 | * multi-copy atomicity,# 491 | * mutability,interior (see interior mutability) 492 | * mutable references,# 493 | * (see also exclusive references) 494 | * Mutex,#,# 495 | * (see also mutexes) 496 | * mutexes,# 497 | * building our own,# 498 | * as container,# 499 | * contention,#,# 500 | * example,# 501 | * fair,# 502 | * happens-before relationship,# 503 | * into_inner,# 504 | * lifetime of mutex guard,# 505 | * memory ordering,# 506 | * Mutex type in Rust,# 507 | * os_unfair_lock (macOS),# 508 | * in other languages,# 509 | * poisoning,# 510 | * pthread 511 | * wrapping in Rust,# 512 | * pthread_mutex_t,# 513 | * recursive,# 514 | * robust,# 515 | * Send requirement,# 516 | * spin locks,# 517 | * (see also spin locks) 518 | * spinning,#,# 519 | * using to build a channel,# 520 | * MutexGuard,# 521 | * dropping,# 522 | * lifetime of,# 523 | * mutual exclusion (see mutexes) 524 | 525 | ### N 526 | 527 | * name of a thread,# 528 | * NetBSD,futex support,# 529 | * (see also futex) 530 | * NonNull,# 531 | 532 | ### O 533 | 534 | * -O flag (rustc),#,# 535 | * Once and OnceLock,#,# 536 | * one-shot channels,# 537 | * OpenBSD,limited futex support,# 538 | * (see also futex) 539 | * operating systems,# 540 | * (see also Linux; macOS; Windows) 541 | * libraries shipped with,# 542 | * synchronization primitives,# 543 | * optimization (see compiler optimization) 544 | * or instruction (x86),#,# 545 | * Ordering,#,# 546 | * AcqRel,# 547 | * (see also release and acquire memory ordering) 548 | * Acquire,# 549 | * (see also release and acquire memory ordering) 550 | * Consume,# 551 | * Relaxed,#,# 552 | * Release,# 553 | * (see also release and acquire memory ordering) 554 | * SeqCst,# 555 | * (see also sequentially consistent memory ordering) 556 | * os_unfair_lock (macOS),# 557 | * other-multi-copy atomicity,# 558 | * out of order execution (see reordering) 559 | * out-of-thin-air values,# 560 | * output locking,# 561 | * overflows (atomic),# 562 | * (see also wrapping behavior) 563 | * aborting on,# 564 | * notification counter,# 565 | * panicking on,# 566 | * preventing (compare-and-exchange),# 567 | * reference counter,# 568 | * usize,big enough,# 569 | * overview of atomic instructions,# 570 | * ownership 571 | * moving,#,# 572 | * sharing,# 573 | * transferring to another thread (Send),# 574 | 575 | ### P 576 | 577 | * panicking 578 | * poisoned mutexes,# 579 | * RefCell,borrowing,# 580 | * thread name in panic messages,# 581 | * using a Condvar with multiple mutexes,# 582 | * when joining a thread,#,# 583 | * when spawning a thread,# 584 | * parking (see thread parking) 585 | * parking lot-based locks,# 586 | * parking_lot crate,# 587 | * PhantomData,#,# 588 | * Pin,# 589 | * pipelining,# 590 | * pointers 591 | * atomic (see AtomicPtr) 592 | * neither Send nor Sync,# 593 | * NonNull,# 594 | * poisoning,lock,# 595 | * POSIX,# 596 | * pthreads,# 597 | * println,use of output locking,# 598 | * priority inheritance,# 599 | * priority inversion,# 600 | * privacy (modules),# 601 | * process-wide memory barriers,# 602 | * processes,# 603 | * processor architecture,# 604 | * (see also ARM64; x86-64) 605 | * strongly ordered,# 606 | * weakly ordered,# 607 | * processor caching (see caching) 608 | * processor instructions (see instructions) 609 | * processor registers,# 610 | * return value,# 611 | * pthreads,# 612 | * pthread_cond_t,#,# 613 | * pthread_mutex_t,# 614 | * dropping while locked,# 615 | * pthread_rwlock_t,# 616 | * wrapping in Rust,# 617 | 618 | ### Q 619 | 620 | * queue-based locks,# 621 | 622 | ### R 623 | 624 | * racing,# 625 | * Rc,# 626 | * RCU (read,copy,update),#,# 627 | * reader-writer locks,# 628 | * avoiding accidental spinning,# 629 | * building our own,# 630 | * pthread_rwlock_t,# 631 | * Send requirement,# 632 | * SRW locks (Windows),# 633 | * Sync requirement,# 634 | * writer starvation,#,# 635 | * recursive locking,#,# 636 | * reduced instruction set computer (RISC),# 637 | * RefCell,# 638 | * RwLock compared to,# 639 | * reference counting,# 640 | * (see also Arc) 641 | * references 642 | * exclusive,# 643 | * immutable,# 644 | * (see also shared) 645 | * mutable,# 646 | * (see also exclusive) 647 | * shared,# 648 | * registers,# 649 | * return value,# 650 | * relaxed memory ordering,#,#,# 651 | * counter-intuitive results,# 652 | * misconceptions about,#,# 653 | * out-of-thin-air values,# 654 | * reference counting,# 655 | * total modification order,#,#,#,# 656 | * release and acquire memory ordering,# 657 | * acquire fence,#,#,#,#,# 658 | * on ARM64,# 659 | * example,lazy initialization,# 660 | * experiment,using relaxed instead,# 661 | * happens-before relationship,#,# 662 | * chaining,# 663 | * locking and unlocking,#,# 664 | * reference counting,#,#,#,# 665 | * release fence,# 666 | * on x86-64,# 667 | * --release flag (cargo),#,# 668 | * reordering (instructions),#,# 669 | * memory ordering,# 670 | * `#[repr(align)]`,# 671 | * requeuing waiting threads,#,# 672 | * ret (return) instruction (ARM),# 673 | * ret (return) instruction (x86),# 674 | * robust mutexes,# 675 | * rustup,# 676 | * RwLock,#,# 677 | * (see also reader-writer locks) 678 | * RwLockReadGuard,# 679 | * RwLockWriteGuard,# 680 | 681 | ### S 682 | 683 | * safe interface,#,#,# 684 | * safety requirements of unsafe functions,# 685 | * scheduler,# 686 | * scoped threads,# 687 | * semaphores,# 688 | * Send trait,#,#,# 689 | * error,# 690 | * implementing for Arc,# 691 | * requirement by Mutex and RwLock,# 692 | * SeqCst (see sequentially consistent memory ordering) 693 | * sequence locks,# 694 | * sequentially consistent memory ordering,# 695 | * on ARM64,# 696 | * fence,# 697 | * misconceptions about,#,# 698 | * on x86-64,# 699 | * shadowing,# 700 | * shared ownership,# 701 | * leaking,# 702 | * reference counting,# 703 | * statics,# 704 | * shared references,# 705 | * mutating atomics through,# 706 | * slim reader-writer locks (Windows),#,# 707 | * spawning threads,# 708 | * failing to,# 709 | * happens-before relationship,# 710 | * scoped,# 711 | * spin locks 712 | * building our own,# 713 | * cache lines,effect of,# 714 | * compare-and-exchange,(not) using,# 715 | * experiment,using wrong memory ordering,# 716 | * guard,# 717 | * memory ordering,# 718 | * spin loop hint,#,# 719 | * spinning,#,#,# 720 | * avoiding accidental (reader-writer lock),# 721 | * splitting (borrowing),# 722 | * spurious wake-ups,#,#,# 723 | * SRW locks (Windows),#,# 724 | * stack size,# 725 | * starvation,#,# 726 | * static lifetime,# 727 | * statics,# 728 | * stlr (store-release register) instruction (ARM),# 729 | * stlxr (store-release exclusive register) instruction (ARM),# 730 | * stop flag,# 731 | * store buffers,# 732 | * store operations (atomic) (see load and store operations) 733 | * store-conditional (see load-linked/store-conditional) 734 | * str (store register) instruction (ARM),# 735 | * stress,reducing,# 736 | * strongly ordered architecture,# 737 | * stxr (store exclusive register) instruction (ARM),# 738 | * sub (subtract) instruction (x86),# 739 | * swap operation (atomic),# 740 | * locking using,# 741 | * Sync trait,#,# 742 | * implementing for Arc,# 743 | * implementing for channel,# 744 | * implementing for mutex,# 745 | * implementing for reader-writer lock,# 746 | * implementing for spin lock,# 747 | * requirement by RwLock,# 748 | * SYS_futex (Linux),# 749 | * (see also futex) 750 | * arguments,# 751 | * syscalls,# 752 | * avoiding,#,# 753 | 754 | ### T 755 | 756 | * --target (rustc),# 757 | * teaching,# 758 | * thin air,out of,# 759 | * thread builder,# 760 | * thread name,# 761 | * Thread object,# 762 | * id,# 763 | * unpark,#,# 764 | * thread parking,#,#,#,# 765 | * spurious wake-ups,# 766 | * timeout,# 767 | * example,# 768 | * thread safety,#,# 769 | * keeping objects on one thread,# 770 | * ThreadId,# 771 | * threads,# 772 | * joining,# 773 | * panicking,#,# 774 | * returning a value,# 775 | * scoped,# 776 | * spawning,# 777 | * thundering herd problem,# 778 | * time travel,# 779 | * timeout 780 | * condition variables,# 781 | * futex,#,# 782 | * thread parking,# 783 | * example,# 784 | * total modification order,#,#,#,# 785 | 786 | ### U 787 | 788 | * uncontended (mutexes),#,# 789 | * benchmarking,# 790 | * undefined behavior,# 791 | * borrowing,# 792 | * data races,# 793 | * Miri,detecting with,# 794 | * time travel,# 795 | * uninitialized memory,# 796 | * Unix systems 797 | * interfacing with the kernel,# 798 | * libc,role of,# 799 | * unmovable 800 | * critical section (Windows),# 801 | * Pin,# 802 | * pthread types,# 803 | * wrapping in Box,# 804 | * unpark (Thread),# 805 | * unparking (see thread parking) 806 | * unsafe code,# 807 | * unsafe functions,# 808 | * unsafe trait implementation,# 809 | * UnsafeCell,#,#,# 810 | * get_mut,# 811 | * unsound,# 812 | 813 | ### V 814 | 815 | * VecDeque,# 816 | 817 | ### W 818 | 819 | * waiting (see blocking) 820 | * WaitOnAddress (Windows),# 821 | * WakeByAddressAll (Windows),# 822 | * WakeByAddressSingle (Windows),# 823 | * Weak (see Arc; weak pointers) 824 | * weakly ordered architecture,# 825 | * experiment,using relaxed instead of release and acquire,# 826 | * Windows,# 827 | * condition variables,# 828 | * critical section,# 829 | * FlushProcessWriteBuffers,# 830 | * interfacing with the kernel,# 831 | * kernel-managed objects,# 832 | * Native API,# 833 | * process-wide memory barrier,# 834 | * SRW locks,#,# 835 | * WaitOnAddress,# 836 | * WakeByAddressAll,# 837 | * WakeByAddressSingle,# 838 | * Win32 API,# 839 | * windows crate,# 840 | * windows-sys crate,# 841 | * wrapping behavior (fetch_add and fetch_sub),# 842 | * (see also overflows (atomic)) 843 | * wrapping unmovable object in Box,# 844 | * write-through cache coherence protocol,# 845 | * writer starvation,#,# 846 | 847 | ### X 848 | 849 | * x86-64 (processor architecture),# 850 | * other-multi-copy atomic,# 851 | * strongly ordered,# 852 | * x86_64-unknown-linux-musl target,# 853 | * x86-64 instructions 854 | * add,# 855 | * and,# 856 | * btc (bit test and complement),# 857 | * btr (bit test and reset),# 858 | * bts (bit test and set),# 859 | * cmpxchg (compare and exchange),# 860 | * jne (jump if not equal),# 861 | * lock prefix,# 862 | * mfence (memory fence),# 863 | * mov (move),# 864 | * or,#,# 865 | * overview,# 866 | * ret (return),# 867 | * sub (subtract),# 868 | * xadd (exchange and add),# 869 | * xchg (exchange),#,# 870 | * xor,# 871 | * xadd (exchange and add) instruction (x86),# 872 | * xchg (exchange) instruction (x86),#,# 873 | * xor instruction (x86),# 874 | 875 | ## 译注 876 | 877 | | 英文 | 中译 | 可能出现章节 | 878 | | -------------------- | -------------- | ------------------- | 879 | | allocation | 内存分配 | 1、3、5、6、8、10 | 880 | | atomic | 原子 | all | 881 | | benchmark | 基准测试 | 4、9 | 882 | | borrow | 借用 | 1、4、5 | 883 | | building block | 基石 | 1、2、8、10 | 884 | | cache coherence | 缓存一致性 | 7 | 885 | | compare and exchange | 比较并交换 | 3、4、6、9 | 886 | | condition variable | 条件变量 | 1、2、5、8、9 | 887 | | drop | 丢弃 | 1、3、4、5、6、9 | 888 | | fetch and modify | 获取并修改 | 1、3 | 889 | | fence | 屏障 | 3、6、7、8 | 890 | | formalize | 形式化的 | 3 | 891 | | guard | 守卫 | 4、9 | 892 | | happens-before | happens-before | 3、4、5、6、7、9 | 893 | | Invalidation queue | 失效队列 | 7 | 894 | | leak | (内存)泄漏 | 1、3、5、8、10 | 895 | | load operation | load 操作 | 2、3、5、6、7、8、9 | 896 | | lock(v) | 锁定 | all | 897 | | memory ordering | 内存排序 | 2、3、4、5、6、7、9 | 898 | | mutex | 互斥锁 | 1、2、3、4、8、9 | 899 | | mutation | 可变性 | 1、2、4、6、8 | 900 | | notify | 通知 | 1、4、5、8、9 | 901 | | notify_all | notify all | 9 | 902 | | notify_one | notify one | 9 | 903 | | park/block | 阻塞 | all | 904 | | pipeline | 流水线 | 7 | 905 | | reader-writer lock | 读写锁 | 1、3、4、8、9 | 906 | | reader | reader | 1、4、8、9、10 | 907 | | receiver | 接收者 | 5 | 908 | | reference | 引用 | all | 909 | | spinLock | 自旋锁 | 3、4、8、9 | 910 | | sender | 发送者 | 5 | 911 | | spurious | 虚假的 | 1、2、5、8、9 | 912 | | static | 静态值 | 1 | 913 | | stop the world | 停止其他活动 | 7 | 914 | | store buffer | 存储缓冲区 | 7 | 915 | | store operation | store 操作 | 2、3、5、6、7、8、9 | 916 | | swap operation | swap 操作 | 2、3、5、6、7、8、9 | 917 | | syscall | 系统调用 | 8、9 | 918 | | unlock(v) | 解锁 | all | 919 | | unpark | 释放 | all | 920 | | use cases | 用例 | all | 921 | | wait(er) | 等待(者) | all | 922 | | writer | writer | 1、4、8、9、10 | 923 | -------------------------------------------------------------------------------- /picture/raal_0301.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a8f8b761df560f5679a2efa0faa00e1468fb81a7ffd6c2ab3e056bb1757286af 3 | size 33554 4 | -------------------------------------------------------------------------------- /picture/raal_0302.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d703a7dc199f1ed82b67987231af8f1b362adc7aba106c45e18246585dd896f8 3 | size 41866 4 | -------------------------------------------------------------------------------- /picture/raal_0303.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:476a491a0b49ea942b4801f016b2d9ae7652fe65903bdc250bce5ddc2be8e201 3 | size 66441 4 | -------------------------------------------------------------------------------- /picture/raal_0304.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c88dac2faa16ed09da8092b04e510b32dfb9539d6856c6fb1da04f3337359260 3 | size 108217 4 | -------------------------------------------------------------------------------- /picture/raal_0305.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0dd68697d92b1bb77e95bdad5cccf5cdfdce71144c3c72f839ba2c455302ad94 3 | size 70595 4 | -------------------------------------------------------------------------------- /picture/raal_0401.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9092dea283164f8cdcaf80ced38891acf60b20b09bdfa437e4f8424986d9bcb9 3 | size 137119 4 | -------------------------------------------------------------------------------- /picture/raal_0701.svg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8cd64dd6cfc5c6bd86a816ce1d01f70e0707a17de33e4a16aef8004c2b70f752 3 | size 106638 4 | -------------------------------------------------------------------------------- /picture/raal_0901.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9256991d9850cd37183aa12e334271d44ade2c036513c156cf1428b3b4242f60 3 | size 146386 4 | -------------------------------------------------------------------------------- /picture/raal_0902.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dc6106f0cd7d97374837df3e19ee807117006e3ee5b4e8dc9778b270f6f12e90 3 | size 84923 4 | -------------------------------------------------------------------------------- /picture/raal_10in01.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:00acbd92d1f3145206905333552cbaa534cded7f425e0aca1a8fcfad90d934c9 3 | size 37640 4 | -------------------------------------------------------------------------------- /picture/raal_10in02.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9db8f6d0b99caab2a8fcdc30331c3b84095f987cf0a1a3dce7e96b4775570e44 3 | size 78509 4 | -------------------------------------------------------------------------------- /picture/raal_10in03.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0193e5bd8d6347b9d8df62126eb456d6a7a58abc5b0a4aadc8e34da2cd746341 3 | size 62541 4 | -------------------------------------------------------------------------------- /picture/raal_10in04.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7136cd87c00c329a4c4288aa5ad4749a329bf66d6020e8f78f92a827c91cd985 3 | size 35036 4 | -------------------------------------------------------------------------------- /picture/raal_10in05.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3bcda62af5cb6aa0f35e0a33fa5c86bf63024661c011892e95022d7ee9b71401 3 | size 50692 4 | -------------------------------------------------------------------------------- /picture/raal_10in06.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d8667ac067404294824f1cd7e7d52903cb009cae2e45edccd725fa909fea5e8d 3 | size 65519 4 | --------------------------------------------------------------------------------