From 3af81d821dbdf7cd5f472b9cf27a5459d45c2d96 Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 13:48:34 +0800 Subject: [PATCH 1/7] Minor update in concurrency-parallelism.md --- .../concurrency-parallelism.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/book/contents/advance/concurrency-with-threads/concurrency-parallelism.md b/book/contents/advance/concurrency-with-threads/concurrency-parallelism.md index 5f314a2e..bc5a4d5d 100644 --- a/book/contents/advance/concurrency-with-threads/concurrency-parallelism.md +++ b/book/contents/advance/concurrency-with-threads/concurrency-parallelism.md @@ -1,6 +1,6 @@ # 并发和并行 -> 并发是同一时间应对多件事情的能力 - [Rob Pike](https://baike.baidu.com/item/罗布·派克/10983505) +> 并发是同一时间应对多件事情的能力 - [Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike) 并行和并发其实并不难,但是也给一些用户造成了困扰,因此我们专门开辟一个章节,用于讲清楚这两者的区别。 @@ -11,15 +11,15 @@ 上图很直观的体现了: -- 并发是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是1:1轮换,也可能是其它轮换规则),最终每个人都能接到咖啡 -- 并行是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡 +- **并发(Concurrent)** 是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是1:1轮换,也可能是其它轮换规则),最终每个人都能接到咖啡 +- **并行(Parallel)** 是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡 当然,我们还可以对比下串行:只有一个队列且仅使用一台咖啡机,哪怕前面那个人接咖啡时突然发呆了几分钟,后面的人也只能等他结束才能继续接。可能有读者有疑问了,从图片来看,并发也存在这个问题啊,前面的人抽了几分钟不接怎么办?很简单,另外一个队列的人把他推开就行了,自己队友不能在背后开枪,但是其它队的可以:) 在正式开始之前,先给出一个结论:**并发和并行都是对"多任务"处理的描述,其中并发是轮流处理,而并行是同时处理**。 ## CPU多核 -现在的个人计算机动辄拥有十来个核心(M1 Max/Itel 12代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个cpu核心去执行,当某个cpu核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。 +现在的个人计算机动辄拥有十来个核心(M1 Max/Intel 12代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个cpu核心去执行,当某个cpu核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。 #### 单核心并发 那问题来了,在早期只有一个CPU核心时,我们的任务是怎么处理的呢?其实聪明的读者应该已经想到,是的,并发解君愁。当然,这里还得提到操作系统的多线程,正是操作系统多线程 + CPU核心,才实现了现代化的多任务操作系统。 @@ -29,7 +29,7 @@ 相信大家都看出来了:**CPU核心**对应的是上图的咖啡机,而**多个线程的任务队列**就对应的多个排队的队列,最终受限于CPU核心数, 每次只会有一个任务被处理。 -和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用windows的同学或多或少都碰到或者假死的问题), 那么就需要CPU的任务调度了(真实CPU和调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。 +和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用Windows的同学或多或少都碰到过假死的问题), 那么就需要CPU的任务调度了(真实CPU的调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。 不断这样的快速任务切换,对用户而言就实现了表面上的多任务同时处理,但是实际上最终也只有一个CPU核心在不停的工作。 @@ -46,9 +46,9 @@ ## 正式的定义 -如果某个系统支持两个或者多个动作的同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于**“存在”**这个词。 +如果某个系统支持两个或者多个动作的**同时存在**,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作**同时执行**,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于 **“存在”** 这个词。 -在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是**同时“存在”**的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。 +在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是 **同时“存在”** 的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。 相信你已经能够得出结论——**“并行”概念是“并发”概念的一个子集**。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。 From 3d195b46923d2986712575fd1ad6b806c5f63fb6 Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 14:39:38 +0800 Subject: [PATCH 2/7] Minor update in thread.md --- .../advance/concurrency-with-threads/thread.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/book/contents/advance/concurrency-with-threads/thread.md b/book/contents/advance/concurrency-with-threads/thread.md index 6763beef..d5661ea8 100644 --- a/book/contents/advance/concurrency-with-threads/thread.md +++ b/book/contents/advance/concurrency-with-threads/thread.md @@ -216,9 +216,9 @@ fn main() { 据不精确估算,创建一个线程大概需要0.24毫秒,随着线程的变多,这个值会变得更大,因此线程的创建耗时并不是不可忽略的,只有当真的需要处理一个值得用线程去处理的任务时,才使用线程。一些鸡毛蒜皮的任务,就无需创建线程了。 #### 创建多少线程合适 -因为CPU的核心数限制,当任务是密集型时,就算线程数超过了CPU核心数,也并不能帮你获得更好的性能,因为每个线程的任务都可以轻松让CPU的某个核心跑满,既然如此,让线程数等于CPU核心数是最好的。 +因为CPU的核心数限制,当任务是CPU密集型时,就算线程数超过了CPU核心数,也并不能帮你获得更好的性能,因为每个线程的任务都可以轻松让CPU的某个核心跑满,既然如此,让线程数等于CPU核心数是最好的。 -但是当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量,这样当某个线程处于阻塞状态时,会被切走,进而运行其它的线程,典型就是网络IO操作,我们可以为每一个进来的用户连接创建一个线程去处理,该连接绝大部分时间都是处于IO读取阻塞状态,因此有限的CPU核心完全可以处理成百上千的用户连接线程,但是事实上,对于这种网络IO情况,一般都不再使用多线程的方式了,毕竟操作系统的线程数是有限的,意味着并发数也很容易达到上限,使用async/await的`M:N`并发模型,就没有这个烦恼。 +但是当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量,这样当某个线程处于阻塞状态时,会被切走,进而运行其它的线程,典型就是网络IO操作,我们可以为每一个进来的用户连接创建一个线程去处理,该连接绝大部分时间都是处于IO读取阻塞状态,因此有限的CPU核心完全可以处理成百上千的用户连接线程,但是事实上,对于这种网络IO情况,一般都不再使用多线程的方式了,毕竟操作系统的线程数是有限的,意味着并发数也很容易达到上限,而且过多的线程也会导致线程上下文切换的代价过大,使用async/await的`M:N`并发模型,就没有这个烦恼。 #### 多线程的开销 下面的代码是一个无锁实现(CAS)的hashmap在多线程下的使用: @@ -333,7 +333,7 @@ FOO.with(|f| { }); ``` -上面代码中,`FOO`即是我们创建的**线程局部变量**,每个新的线程访问它时,都会使用它的初始值作为开始,各个线程中的`FOO`值彼此互不干扰。 +上面代码中,`FOO`即是我们创建的**线程局部变量**,每个新的线程访问它时,都会使用它的初始值作为开始,各个线程中的`FOO`值彼此互不干扰。注意`FOO`使用`static`声明为生命周期为`'static`的静态变量。 可以注意到,线程中对`FOO`的使用是通过借用的方式,但是若我们需要每个线程独自获取它的拷贝,最后进行汇总,就有些强人所难了。 @@ -434,8 +434,8 @@ fn main() { 上述代码流程如下: -1. `main`线程首先进入`while`循环,并释放了锁`started`,然后开始挂起等待子线程的通知 -2. 子线程获取到锁,并将其修改为true, 然后调用条件的方法来通知主线程继续执行:`cvar.notify_one` +1. `main`线程首先进入`while`循环,调用`wait`方法挂起等待子线程的通知,并释放了锁`started` +2. 子线程获取到锁,并将其修改为true, 然后调用条件变量的`notify_one`方法来通知主线程继续执行 ## 只被调用一次的函数 有时,我们会需要某个函数在多线程环境下只被调用一次,例如初始化全局变量,无论是哪个线程先调用函数来初始化,都会保证全局变量只会被初始化一次,随后的其它线程调用就会忽略该函数: @@ -472,6 +472,14 @@ fn main() { 代码运行的结果取决于哪个线程先调用`INIT.call_once`(虽然代码具有先后顺序,但是线程的初始化顺序并无法被保证!因为线程初始化是异步的,且耗时较久),若`handle1`先,则输出`1`,否则输出`2`。 +**call_once 方法** + +执行初始化过程一次,并且只执行一次。 + +如果当前有另一个初始化过程正在运行,该方法将阻止调用的线程。 + +当这个函数返回时,保证一些初始化已经运行并完成,它还保证由执行的闭包所执行的任何内存写入都能被其他线程在这时可靠地观察到。 + ## 总结 [Rust的线程模型](./intro.md)是`1:1`模型,因为Rust要保持尽量小的运行时。 From fe9859088625bee115c0ebf01daa11f6de545eb1 Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 15:14:32 +0800 Subject: [PATCH 3/7] Minor update in message-passing.md --- .../concurrency-with-threads/message-passing.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/book/contents/advance/concurrency-with-threads/message-passing.md b/book/contents/advance/concurrency-with-threads/message-passing.md index 0c38f023..f9a55fc1 100644 --- a/book/contents/advance/concurrency-with-threads/message-passing.md +++ b/book/contents/advance/concurrency-with-threads/message-passing.md @@ -102,7 +102,7 @@ fn main() { } ``` -以上代码中,`String`底层的字符串是存储在堆上,被没有实现`Copy`特征,当它被发送后,会将所有权从发送端的`s`转移给接收端的`received`,之后`s`将无法被使用: +以上代码中,`String`底层的字符串是存储在堆上,并没有实现`Copy`特征,当它被发送后,会将所有权从发送端的`s`转移给接收端的`received`,之后`s`将无法被使用: ```console error[E0382]: borrow of moved value: `s` --> src/main.rs:10:31 @@ -178,10 +178,10 @@ fn main() { - 需要所有的发送者都被`drop`掉后,接收者`rx`才会收到错误,进而跳出`for`循环,最终结束主线程 - 这里虽然用了`clone`但是并不会影响性能,因为它并不在热点代码路径中,仅仅会被执行一次 -- 由于两个子线程谁创建完成是未知的,因此哪条消息先发送也是未知的,最终主线程的输出顺序也不确定 +- 由于两个子线程谁先创建完成是未知的,因此哪条消息先发送也是未知的,最终主线程的输出顺序也不确定 ## 消息顺序 -上述第三点的消息顺序仅仅是因为线程创建引起的,并不代表通道中的线程是无序的,对于通道而言,消息的发送顺序和接收顺序是一直的,满足`FIFO`原则(先进先出)。 +上述第三点的消息顺序仅仅是因为线程创建引起的,并不代表通道中的消息是无序的,对于通道而言,消息的发送顺序和接收顺序是一致的,满足`FIFO`原则(先进先出)。 由于篇幅有限,具体的代码这里就不再给出,感兴趣的读者可以自己验证下。 @@ -228,7 +228,7 @@ fn main() { 从输出还可以看出,`发送之前`和`发送之后`是连续输出的,没有受到接收端主线程的任何影响,因此通过`mpsc::channel`创建的通道是异步通道。 #### 同步通道 -与异步通道相反,同步通道**发送消息是阻塞的,只有在消息被接收后才解除阻塞**例如: +与异步通道相反,同步通道**发送消息是阻塞的,只有在消息被接收后才解除阻塞**,例如: ```rust use std::sync::mpsc; use std::thread; @@ -370,8 +370,8 @@ fn main() { 解决办法很简单,`drop`掉`send`即可:在代码中的注释下面添加一行`drop(send);`。 -## mpmc、更好的性能 +## mpmc 更好的性能 如果你需要mpmc(多发送者,多接收者)或者需要更高的性能,可以考虑第三方库: - [**crossbeam-channel**](https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel), 老牌强库,功能较全,性能较强,之前是独立的库,但是后面合并到了`crossbeam`主仓库中 -- [**flume**](https://github.com/zesterer/flume), 官方给出的性能数据要比crossbeam更好些,但是貌似最近没怎么更新 +- [**flume**](https://github.com/zesterer/flume), 官方给出的性能数据某些场景要比crossbeam更好些 From 5007f82840edc9d361bb90a75e9ec873bb11953a Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 15:43:14 +0800 Subject: [PATCH 4/7] Fix typo in sync1.md --- book/contents/advance/concurrency-with-threads/sync1.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/book/contents/advance/concurrency-with-threads/sync1.md b/book/contents/advance/concurrency-with-threads/sync1.md index 9f5e68b0..05ffc1bb 100644 --- a/book/contents/advance/concurrency-with-threads/sync1.md +++ b/book/contents/advance/concurrency-with-threads/sync1.md @@ -248,7 +248,7 @@ fn main() { 在上面的描述中,我们用了"可能"二字,原因在于死锁在这段代码中不是必然发生的,总有一次运行你能看到最后一行打印输出。这是由于子线程的初始化顺序和执行速度并不确定,我们无法确定哪个线程中的锁先被执行,因此也无法确定两个线程对锁的具体使用顺序。 -但是,可以简单的说明下死锁发生的必然条件:线程1锁住了`mutex1`并且线程`2`锁住了`mutex2`,然后线程1试图去访问`mutex2`,同时线程`2`试图去访问`mutex1`,就会锁住。 因为线程2需要等待线程1释放`mutex1`后,才会释放`mutex2`,而与此同时,线程1需要等待线程2释放`mutex2`后才能释放`mutex1`,这种情况造成了两个线程都无法释放对方需要的锁,最终锁死。 +但是,可以简单的说明下死锁发生的必然条件:线程1锁住了`mutex1`并且线程`2`锁住了`mutex2`,然后线程1试图去访问`mutex2`,同时线程`2`试图去访问`mutex1`,就会死锁。 因为线程2需要等待线程1释放`mutex1`后,才会释放`mutex2`,而与此同时,线程1需要等待线程2释放`mutex2`后才能释放`mutex1`,这种情况造成了两个线程都无法释放对方需要的锁,最终死锁。 那么为何某些时候,死锁不会发生?原因很简单,线程2在线程1锁`mutex1`之前,就已经全部执行完了,随之线程2的`mutex2`和`mutex1`被全部释放,线程1对锁的获取将不再有竞争者。 同理,线程1若全部被执行完,那线程2也不会被锁,因此我们在线程1中间加一个睡眠,增加死锁发生的概率。如果你在线程2中同样的位置也增加一个睡眠,那死锁将必然发生! @@ -307,7 +307,7 @@ fn main() { } ``` -为了演示`try_lock`的作用,我们特定使用了之前必定会死锁的代码,并且将`lock`替换程`try_lock`,与之前的结果不同,这段代码将不会再有死锁发生: +为了演示`try_lock`的作用,我们特定使用了之前必定会死锁的代码,并且将`lock`替换成`try_lock`,与之前的结果不同,这段代码将不会再有死锁发生: ```console 线程 0 锁住了mutex1,接着准备去锁mutex2 ! 线程 1 锁住了mutex2, 准备去锁mutex1 @@ -396,7 +396,7 @@ Err("WouldBlock") 如果不是追求特别极致的性能,建议选择前者。 -## 用条件(Condvar)控制线程的同步 +## 用条件变量(Condvar)控制线程的同步 `Mutex`用于解决资源安全访问的问题,但是我们还需要一个手段来解决资源访问顺序的问题。而Rust考虑到了这一点,为我们提供了条件变量(Condition Variables),它经常和`Mutex`一起使用,可以让线程挂起,直到某个条件发生后再继续执行,其实`Condvar`我们在之前的多线程章节就已经见到过,现在再来看一个不同的例子: @@ -474,7 +474,7 @@ async fn main() { let mut join_handles = Vec::new(); for _ in 0..5 { - let permit = semaphore.clone().acquire_owned().await.unwrap(); + let permit = semaphore.clone().acquire_owned().await.unwrap(); join_handles.push(tokio::spawn(async move { // // 在这里执行任务... From 44aedc8e6be83c98d61751b8c9ab2194e20d720e Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 16:34:04 +0800 Subject: [PATCH 5/7] Update content in sync2.md --- .../advance/concurrency-with-threads/sync2.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/book/contents/advance/concurrency-with-threads/sync2.md b/book/contents/advance/concurrency-with-threads/sync2.md index a9cc7c37..096a9228 100644 --- a/book/contents/advance/concurrency-with-threads/sync2.md +++ b/book/contents/advance/concurrency-with-threads/sync2.md @@ -138,7 +138,7 @@ Y = 3; Y *= 2; X = 2; } ``` -我们来讨论下以上线程状态下,`Y`最终的可能值(可能性依次降低): +我们来讨论下以上线程状态,`Y`最终的可能值(可能性依次降低): - `Y = 3`: 线程`Main`运行完后才运行线程`A`,或者线程`A`运行完后再运行线程`Main` - `Y = 6`: 线程`Main`的`Y = 3`运行完,但`X = 2`还没被运行, 此时线程A开始运行`Y *= 2`, 最后才运行`Main`线程的`X = 2` @@ -161,10 +161,10 @@ X = 2; } 在理解了内存顺序可能存在的改变后,你就可以明白为什么Rust提供了`Ordering::Relaxed`用于限定内存顺序了,事实上,该枚举有5个成员: - **Relaxed**, 这是最宽松的规则,它对编译器和CPU不做任何限制,可以乱序 -- **Release**,设定内存屏障(Memory barrier),指定屏障之前的数据不能被重新排序 -- **Acquire**, 设定内存屏障,指定屏障之后的数据不能被重新排序,往往和`Release`在不同线程中联合使用 +- **Release 释放**,设定内存屏障(Memory barrier),保证它之前的操作永远在它之前,但是它后面的操作可能被重排到它前面 +- **Acquire 获取**, 设定内存屏障,保证在它之后的访问永远在它之后,但是它之前的操作却有可能被重排到它后面,往往和`Release`在不同线程中联合使用 - **AcqRel**, **Acquire**和**Release**的结合,同时拥有它们俩提供的保证。比如你要对一个 `atomic` 自增 1,同时希望该操作之前和之后的读取或写入操作不会被重新排序 -- **SeqCst**, `SeqCst`就像是`AcqRel`的加强版,它不管原子操作是属于读取还是写入的操作,只要某个线程有用到`SeqCst`的原子操作,线程中该`SeqCst`操作前的数据操作绝对不会被重新排在该`SeqCst`操作之后,且该`SeqCst`操作后的数据操作也绝对不会被重新排在`SeqCst`操作前。 +- **SeqCst 顺序一致性**, `SeqCst`就像是`AcqRel`的加强版,它不管原子操作是属于读取还是写入的操作,只要某个线程有用到`SeqCst`的原子操作,线程中该`SeqCst`操作前的数据操作绝对不会被重新排在该`SeqCst`操作之后,且该`SeqCst`操作后的数据操作也绝对不会被重新排在`SeqCst`操作前。 这些规则由于是系统提供的,因此其它语言提供的相应规则也大同小异,大家如果不明白可以看看其它语言的相关解释。 @@ -217,6 +217,11 @@ fn main() { 原则上,`Acquire`用于读取,而`Release`用于写入。但是由于有些原子操作同时拥有读取和写入的功能,此时就需要使用`AcqRel`来设置内存顺序了。在内存屏障中被写入的数据,都可以被其它线程读取到,不会有CPU缓存的问题。 +**内存顺序的选择** + +1. 不知道怎么选择时,优先使用`SeqCst`,虽然会稍微减慢速度,但是慢一点也比出现错误好 +2. 多线程只计数`fetch_add`而不使用该值触发其他逻辑分支的简单使用场景,可以使用`Relaxed` +参考 [Which std::sync::atomic::Ordering to use?](https://stackoverflow.com/questions/30407121/which-stdsyncatomicordering-to-use) ## 多线程中使用Atomic From 46a43a82de03f1ac71ca51d90b63cd53ff0c605d Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 16:55:13 +0800 Subject: [PATCH 6/7] Minor update in send-sync.md --- book/contents/advance/concurrency-with-threads/send-sync.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/contents/advance/concurrency-with-threads/send-sync.md b/book/contents/advance/concurrency-with-threads/send-sync.md index d83ae4cf..3d1b5cd9 100644 --- a/book/contents/advance/concurrency-with-threads/send-sync.md +++ b/book/contents/advance/concurrency-with-threads/send-sync.md @@ -76,6 +76,8 @@ unsafe impl Sync for Mutex {} 当然,如果是自定义的复合类型,那没实现那哥俩的就较为常见了:**只要复合类型中有一个成员不是`Send`或`Sync`,那么该符合类型也就不是`Send`或`Sync`**。 +**手动实现 `Send` 和 `Sync` 是不安全的**,通常并不需要手动实现 Send 和 Sync trait,实现者需要使用`unsafe`小心维护并发安全保证。 + 至此,相关的概念大家已经掌握,但是我敢肯定,对于这两个滑不溜秋的家伙,大家依然会非常模糊,不知道它们该如何使用。那么我们来一起看看如何让原生指针可以在线程间安全的使用。 ## 为原生指针实现`Send` From 6bc6c5da814855da29e7e78f7e601e6efba08be3 Mon Sep 17 00:00:00 2001 From: lijinpeng Date: Sun, 23 Jan 2022 17:26:47 +0800 Subject: [PATCH 7/7] Update content in global-variable.md --- book/contents/advance/global-variable.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/book/contents/advance/global-variable.md b/book/contents/advance/global-variable.md index 028304cb..4a6d5b29 100644 --- a/book/contents/advance/global-variable.md +++ b/book/contents/advance/global-variable.md @@ -17,6 +17,14 @@ fn main() { } ``` +**常量与普通变量的区别** +- 关键字是`const`而不是`let` +- 定义常量必须指明类型(如i32)不能省略 +- 定义常量时变量的命名规则一般是全部大写 +- 常量可以在任意作用域进行定义,其生命周期贯穿整个程序的生命周期。编译时编译器会尽可能将其内联到代码中,所以在不同地方对同一常量的引用并不能保证引用到相同的内存地址 +- 常量的赋值只能是常量表达式/数学表达式,也就是说必须是在编译期就能计算出的值,如果需要在运行时才能得出结果的值比如函数,则不能赋值给常量表达式 +- 对于变量出现重复的定义(绑定)会发生变量遮盖,后面定义的变量会遮住前面定义的变量,常量则不允许出现重复的定义 + #### 静态变量 静态变量允许声明一个全局的变量,常用于全局数据统计,例如我们希望用一个变量来统计程序当前的总请求数: ```rust @@ -33,6 +41,12 @@ Rust要求必须使用`unsafe`语句块才能访问和修改`static`变量,因 只有在同一线程内或者不在乎数据的准确性时,才应该使用全局静态变量。 +和常量相同,定义静态变量的时候必须赋值为在编译期就可以计算出的值(常量表达式/数学表达式),不能是运行时才能计算出的值(如函数) + +**静态变量和常量的区别** +- 静态变量不会被内联,在整个程序中,静态变量只有一个实例,所有的引用都会指向同一个地址 +- 存储在静态变量中的值必须要实现Sync trait + #### 原子类型 想要全局计数器、状态控制等功能,又想要线程安全的实现,原子类型是非常好的办法。 @@ -127,6 +141,8 @@ fn main() { 当然,使用`lazy_static`在每次访问静态变量时,会有轻微的性能损失,因为其内部实现用了一个底层的并发原语`std::sync::Once`,在每次访问该变量时,程序都会执行一次原子指令用于确认静态变量的初始化是否完成。 +`lazy_static`宏,匹配的是`static ref`,所以定义的静态变量都是不可变引用 + 可能有读者会问,为何需要在运行期初始化一个静态变量,除了上面的全局锁,你会遇到最常见的场景就是:**一个全局的动态配置,它在程序开始后,才加载数据进行初始化,最终可以让各个线程直接访问使用** 再来看一个使用`lazy_static`实现全局缓存的例子: