深入浅出 Rust笔记 Series 4
系列导航
这是我在阅读范长春的《深入浅出 Rust》时做的笔记,绝大部分内容源自此书,另有小部分内容源自 Rust 圣经。这两份资料是我入门 Rust 的主要材料。
第四部分 线程安全
chap 27 线程安全
-
std::thread API
-
创建线程。默认情况下,新创建的子线程与原来的父线程是分离的关系。子线程可以在父线程结束后继续存在,除非父线程是主线程。如果一个进程的主线程也退出了,这个进程就会终止,其他所有的线程也会随之结束。
1
2
3
4// child 的类型是 JoinHandle<T>,这个 T 是闭包的返回类型
let child = thread::spawn(move || {
// 这里是新建线程的执行逻辑
}); -
等待子线程执行结束
.join()
-
Builder 模式可为子线程指定更多的参数信息
1
2
3thread::Builder::new().name("child1".to_string()).spawn(move || {
println!("Hello, world!");
}); -
thread::sleep(dur: Duration)
使得当前线程等待一段时间继续执行。在等待的时间内,线程调度器会调度其他的线程来执行。 -
thread::current()
获得当前的线程。 -
thread::yield_now()
放弃当前线程的执行,要求线程调度器执行线程切换。 -
thread::park()
暂停当前线程,进入等待状态。当thread::Thread::unpark(&self:: Thread
方法被调用的时候,这个线程可以被恢复执行。 -
thread::Thread::unpark(&self::Thread)
恢复一个线程的执行
-
-
Rust 规定不能在多线程中直接读写普通的共享变量,除非使用 Rust 提供的线程安全相关的设施。
-
“data race”即数据竞争,意思是在多线程程序中,不同线程在没有使用同步的条件下并行访问同一块数据,且其中至少有一个是写操作的情况。
-
数据竞争的发生需要三个条件:数据共享——有多个线程同时访问一份数据;数据修改——至少存在一个线程对数据做修改;没有同步——至少存在一个线程对数据的访问没有使用同步措施。
-
只要让这三个条件无法同时发生即可避免竞态条件
- 可以禁止数据共享,比如 actor-based concurrency,多线程之间的通信仅靠发送消息来实现,而不是通过共享数据来实现;
- 可以禁止数据修改,比如 functional programming,许多函数式编程语言严格限制了数据的可变性,而对共享性没有限制。
-
Rust 允许存在可变变量,允许存在状态共享,同时也做到了完整无遗漏的线程安全检查。因为 Rust 设计的一个核心思想就是“共享不可变,可变不共享”,然后再加上类型系统和合理的 API 设计,就可以保证共享数据在访问时一定使用了同步措施。Rust 既可以支持多线程数据共享的风格,也可以支持消息通信的风格。无论选择哪种方案,编译器都能保证没有数据竞争。
-
-
Rust 线程安全背后的功臣是两个特殊的 trait:
std::marker::Sync
(如果类型 T 实现了 Sync 类型,那说明在不同的线程中使用&T 访问同一个变量是安全的);std::marker::Send
(如果类型 T 实现了 Send 类型,那说明这个类型的变量在不同的线程中传递所有权是安全的)。Rust 中所有跟多线程有关的 API,会根据情况,要求类型必须满足 Sync 或者 Send 的约束。
chap 28 详解 Send 和 Sync
chap 29 状态共享
chap 30 管道
-
异步管道
std::sync::mpsc::channel
:发送端和接收端之间存在一个缓冲区(不限长度的缓冲区,可以一直往里面填充数据,直至内存资源耗尽。),发送端发送数据的时候,是先将这个数据扔到缓冲区,再由接收端自己去取。因此,每次发送,立马就返回了,发送端不用管数据什么时候被接收端处理。接口:pub fn channel<T>() -> (Sender<T>, Receiver<T>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15use std::sync::mpsc::channel;
use std::thread;
fn main() {
let (tx, rx) = channel();
for i in 0..10 {
let tx = tx.clone(); // 复制一个新的 tx,将这个复制的变量 move 进入子线程
thread::spawn(move || {
tx.send(i).unwrap();
});
}
drop(tx);
while let Ok(r) = rx.recv() {
println!("received {}", r);
}
}-
Sender 和 Receiver 的泛型参数必须满足 T: Send 约束。这个条件是显而易见的:被发送的消息会从一个线程转移到另外一个线程,这个约束是为了满足线程安全。如果用户指定的泛型参数没有满足条件,在编译的时候会发生错误。
-
发送者调用 send 方法,接收者调用 recv 方法,返回类型都是 Result 类型,用于错误处理,因为它们都有可能调用失败。当发送者已经被销毁的时候,接收者调用 recv 则会返回错误;同样,当接收者已经销毁的时候,发送者调用 send 也会返回错误。
-
管道还可以是多发送端单接收端。做法很简单,只需将发送端 Sender 复制多份即可。复制方式是调用 Sender 类型的 clone() 方法。这个库不支持多接收端的设计,因此 Receiver 类型没有 clone() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17use std::thread;
use std::sync::mpsc::channel;
fn main() {
let (tx, rx) = channel();
for i in 0..10 {
// 复制一个新的 tx,将这个复制的变量 move 进入子线程
let tx = tx.clone();
thread::spawn(move|| {
tx.send(i).unwrap();
});
}
// 如果没有手动 drop 掉 sender 则程序永远不会停止
drop(tx);
while let Ok(r) = rx.recv() {
println!("received {}", r);
}
}
-
-
同步管道
std::sync::mpsc::sync_channel
:其内部有一个固定大小的缓冲区,用来缓存消息。如果缓冲区被填满了,继续调用 send 方法的时候会发生阻塞,等待接收端把缓冲区内的消息拿走才能继续发送。缓冲区的长度可以在建立管道的时候设置,而且 0 是有效数值。如果缓冲区的长度设置为 0,那就意味着每次的发送操作都会进入等待状态,直到这个消息被接收端取走才能返回。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!