add chapter_3_20

This commit is contained in:
Davirain
2023-05-17 18:09:44 +08:00
parent e24b555b87
commit 28a5489df4
2 changed files with 377 additions and 0 deletions

View File

@@ -13,6 +13,7 @@
- [所有权介绍](./chapter_3/chapter_3_7_1.md)
- [引用与借用](./chapter_3/chapter_3_7_2.md)
- [Slice类型](./chapter_3/chapter_3_7_3.md)
- [3.20 Rust并发编程](./chapter_3/chapter_3_20.md)
- [3.21 unsafe编程](./chapter_3/chapter_3_21.md)
- [Rust使用技巧]()
- [Rust代码风格与格式化](./chapter_4/chapter_4_1.md)

View File

@@ -0,0 +1,376 @@
# 3.20 Rust并发编程
## 3.20.1 使用线程
1. 相关概念
- 进程是资源分配的最小单位线程是CPU调度的最小单位。
- 在使用多线程时,经常会遇到如下一些问题:
- 竞争状态:多个线程以不一致的顺序访问数据或资源;
- 死锁:两个线程相互等待对方停止使用其所拥有的资源,造成两者都永久等待;
- 只会发生在特定情况下且难以稳定重现和修复的bug。
- 编程语言提供的线程叫做绿色线程如go语言在底层实现了M:N的模型即M个绿色线程对应N个OS线程。但是Rust标准库只提供11的线程模型的实现即一个Rust线程对应一个Os线程。
2. 创建线程
创建一个新线程需要调用thread::spawn函数并传递一个闭包示例如下
```rust
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| { // 创建一个线程
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
```
3. 等待线程结束
前面的例子并不能保证子线程执行完所有的打印因为主线程有可能在子线程执行完成前结束从而退出程序所以需要在主线程中等待子线程结束。等待子线程结束需要调用join方法示例如下
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // 等待子线程结束
}
```
4. 线程与move闭包
move关键字可用于传递给thread::spawn的闭包获取环境中的值的所有权从而达到将值的所有权从一个线程传送到另一个线程的目的。示例如下
```rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || { // 将v移动进了闭包完成了值从主线程到子线程的过程
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
```
## 3.20.2 传递消息
1. 通道简单介绍
Rust中实现消息传递并发的主要工具是通道。通道由发送者和接受者两部分组成
- 发送者用来发送消息;
- 接收者用来接收消息;
- 发送者或者接收者任一被丢弃时就认为通道被关闭了。
Rust标准库中提供的通道叫做mpsc是多个生产者单个消费者的通道其使用示例如下
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel(); //创建channel返回发送者、接收者
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap(); //使用发送者通过channel发送
});
let received = rx.recv().unwrap(); //使用接收者通过channel接收
println!("Got: {}", received);
}
```
关于mpsc通道的使用有以下几点说明
- 发送者的send方法返回一个Result类型如果接收端已经被丢弃了将没有发送值的目标所以发送操作将返回错误
- 接收者的recv方法也返回Result类型当通道发送端关闭时将返回一个错误值表明不会再由新的值到来了
- 接收还可以使用try_recv方法recv方法会阻塞到一直等待到消息到来而try_recv不会阻塞它会立即返回Ok值标识包含可用信息而Err则代表此时没有任何信息。
2. 通道和所有权
在使用通道时send 函数会获取参数的所有权并移动这个值归接收者所有。例如下面的代码将会编译错误:
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {}", val); //错误此处不能使用val因为val的所有权已经move到通道里面去了
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
```
3. 发送多个值示例
利用通道发送多个值的示例如下:
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}
```
4. 多个生产者示例
mpsc是multiple producer, single consumer的缩写此通道可以有多个生产者。相对应的spmc通道则可以有单个生产者多个消费者。下面的示例演示多个生产者、单个消费者一起工作
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = mpsc::Sender::clone(&tx); //通过clone来使用
thread::spawn(move || {
//第一个发送者线程
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
//第二个发送者线程
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}
```
上面代码中,有两个发送者发送数据,一个接收者结束数据。
## 3.20.3 共享内存
某些情况下多个线程之间需要共享内存即多个线程都访问某个变量。如何在多个线程间安全的共享内存就需要用到锁、原子操作等机制下面主要介绍Rust中锁的使用。
1. 互斥锁Mutex
在任意时刻互斥锁只允许一个线程访问数据在使用数据前需获取锁使用完成后需要释放锁。关于Mutex的用法示例如下
```rust
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6; // 可以对Mutex内部的值进行修改可见Mutex和RefCell类似提供内部可变性
} // 离开作用域Mutex<T>的锁会自动释放
println!("m = {:?}", m);
}
```
Mutex本质上是一个智能指针lock()返回一个叫做MutexGuard的智能指针其内部提供了drop方法所以当MutexGuard离开作用域时会自动释放锁。Mutex获取到锁后可以对其内部的值进行修改可见它和RefCell类似也提供了内部可变性。
2. 多线程与多所有权
有了Mutex之后能对共享数据进行保护但是由于Rust的所有权机制每个值都有且仅有一个所有者。下面的代码将无法通过编译
```rust
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || { // 不能将counter移到多个线程中
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
```
因此要在多个线程间共享内存还需要有一种机制能让多个线程都能访问Mutex保护的数据。根据本书目前学到的知识可以想到Rc智能指针。上面的代码使用Rc智能指针后的示例如下
```rust
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
```
上面的代码仍将编译错误因为Rc是非线程安全的此时需要使用Arc类型。Arc是一个类似于Rc并可以安全的用于并发环境的类型使用Arc的示例如下
```rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // 使用Arc共享
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
```
3. 读写锁
读变量并不会改变变量的状态互斥锁Mutex每次读写都会加锁当在有大量读、少量写的场景时使用Mutex就会效率不高此时可以使用读写锁RwLock示例如下
```rust
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let shared_data = Arc::new(RwLock::new(vec![1, 2, 3])); // 创建一个共享的 vector 数据
let mut threads = vec![];
// 创建 5 个读线程
for i in 0..5 {
let shared_data = shared_data.clone();
threads.push(thread::spawn(move || {
// 获取读锁
let shared_data = shared_data.read().unwrap();
println!("Thread {} read data: {:?}", i, *shared_data);
}));
}
// 创建 2 个写线程
for i in 0..2 {
let shared_data = shared_data.clone();
threads.push(thread::spawn(move || {
// 获取写锁
let mut shared_data = shared_data.write().unwrap();
println!("Thread {} write data", i);
shared_data.push(i); // 向 vector 中添加数据
}));
}
// 等待所有线程完成
for thread in threads {
thread.join().unwrap();
}
}
```
4. 小结
关于Mutex和RwLock的选择
- 追求高并发读取时使用RwLock因为Mutex一次只允许一个线程读取
- 如果要保证写操作的成功性使用Mutex
- 不知道哪个合适统一使用Mutex。
关于Mutex和Arc类型
- Mutex和RefCell一样具有内部可变性不过Mutex是线程安全的而RefCell不是线程安全的
- Arc和Rc功能类似但是Arc是线程安全的而Rc不是线程安全的。
## 3.20.4 Send trait和Sync trait
1. Send和Sync
Send trait和Sync trait是Rust语言中的两个标记trait即未定义任何行为但是可以标记一个类型其作用如下
- 实现了Send的类型可以在线程间安全的传递其所有权
- 实现了Sync的类型可以在线程间通过引用安全的共享。
实现Sync的类型是通过引用在线程间共享的因此一个类型要在线程间安全的共享那么它的应用必须能安全的在线程间进行传递。所以可以有结论若&T满足Send那么T满足Sync。
2. 实现了Send和Sync的类型
Rust中几乎所有类型都默认实现了Send和Sync。Send和Sync也是可自动派生的trait因此一个复合类型如果其内部成员都实现了Send或Sync那么它就自动实现了Send或Sync。
Rust中绝大多数类型都实现了Send和Sync但是下面几个是没有实现Send或者Sync的:
- 裸指针既没有实现Send也没有实现Sync因为它本身就没有任何安全保证的
- UnsafeCell没有实现SyncCell和RefCell也没有实现Sync但是它们实现了Send
- Rc既没有实现Send也没有实现Sync。
通常情况下不需要为某个类型手动实现Send和Sync trait手动实现这些标记trait 涉及到编写不安全的Rust代码本书在后面unsafe编程部分介绍。