Rust从0到1-并发-状态共享
虽然利用消息传递处理并发是一种很好的方式,但并不是唯一的。让我们重新思考一下 Go 语言文档中提出的:“不要通过内存共享进行通讯”(do not communicate by sharing memory.)。那么,通过内存共享通讯会是什么样子的?是什么原因不这样做,而是反过来“通过通讯来共享内存”(share memory by communicating)?
从某方面来说,任何编程语言中的 channel 都类似于单一所有权,因为将一个数据通过 channel 发送出去以后,将无法再使用“这个”数据。而内存共享类似于多所有权:多个线程可以同时访问内存中的同一个位置。就像我们前面介绍过的,智能指针使得多所有权成为可能,这会增加额外的复杂性,因为我们需管理这些不同的所有者。而Rust 的类型系统和所有权规则在正确管理这些所有权方面给予我们极大的帮助。下面,让我们以 mutexes(互斥器、互斥锁或互斥量),用于内存共享中常见的并发原语(primitives,在学习操作系统的时候大家可能会比较常看到这个词,我理解为底层的一些概念和操作,在此之上我们可以封装更高级的行为),为例子来看看。
使用 Mutexes 控制并发
Mutex 的全称是 mutual exclusion,在任意时刻,它只允许一个线程对数据进行访问。为了访问 mutex 中的数据,线程首先需要尝试获得互斥锁(mutex's lock)。锁是 mutex 的一部分,它用于记录当前谁有享有数据的访问权(排他的)。因此,mutex 是通过锁机制来保护数据。
众所周知,mutex 很难用,因为我们必须需要记住以下两条规则:
在使用数据之前必须先尝试获取锁。
在使用完数据之后,必须要释放锁,这样其它线程才能获取锁。
以现实中的例子类比,可以想象以下场景:在某次小组会议上,只有一个麦克风。而如果要发言,必须先要求或示意需要使用麦克风。在获得麦克风后,就可畅所欲言,发言结束后需要将麦克风交给下一位要发言的成员。如果某位成员在结束发言后忘记交还麦克风,那么其他人都将无法发言。可见,如果对麦克风的管理出现问题,会议将无法按计划进行下去!(这个大家可能在一些线上会议中会遇到过类似场景)
正确的管理 mutexes 可能会非常的复杂,这也是为什么许多人倾向于使用 channel。不然怎样,在 Rust 中,非常庆幸有类型系统和所有权规则,至少我们不会在加锁和解锁上出错。
Mutex的 API
让我们先从单线程开始展示如何使用 Mutex,参考下面面的例子:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {:?}", m);
}
在上面的例子中,我们通过关联函数 new 创建了一个 Mutex
当我们获得锁以后,就可以将返回值 num 看作是 m 中数据的可变引用(前面介绍过强制隐式转换)。类型系统确保我们必须先获得锁才能使用 m 中的值:Mutex
你可能会觉得 Mutex
运行上面的例子,我们可以看到将打印出结果 m = 6。
多个线程之间共享 Mutex
下面我们将尝试通过 Mutex
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 || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
在上面的例子中,我们创建了一个 Mutex
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait
...
9 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
10 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state`
To learn more, run the command again with --verbose.
编译器提示我们 counter 在上一次循环中已经被移动到了一个线程里,因此,无法在移动到当前的线程里。也就是说,counter 的需要可以支持多所有权。下面让我们尝试通过前面介绍的获得多所有权的方法来修复这个问题。
让多个线程获得所有权
在前面章节我们介绍过使用智能指针 Rc
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());
}
再次编译,还是无法通过,我们会得到类似下面的错误(编译器真是我们的好老师):
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc>` cannot be sent between threads safely
--> src/main.rs:11:22
|
11 | let handle = thread::spawn(move || {
| ______________________^^^^^^^^^^^^^_-
| | |
| | `Rc>` cannot be sent between threads safely
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________- within this `[closure@src/main.rs:11:36: 15:10]`
|
= help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc>`
= note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state`
To learn more, run the command again with --verbose.
编译器告诉我们 `std::rc::Rc
现在我们清楚的知道 Rc
使用 Arc 进行原子引用计数
Arc
我们可能不禁会想为什么不是所有的基本类型都是原子的?为什么标准库中的类型不都使用 Arc
Arc
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
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());
}
现在运行上面的例子,我们会得到类似下面的结果:
Result: 10
我们做到了!这个例子本身可能比较简单,不过通过它我们一步步了解了关于 Mutex
RefCell/Rc 与 Mutex/Arc
我们可能已经注意到了,在前面的例子中,counter 是不可变的,不过我们可以获取其内部值的可变引用,也就是说 Mutex
另一个需要我们注意的是 Rust 并不能避免在使用 Mutex