除了 channel,我们也可以通过share memory 来进行多线程通信,它的特点是 multiple ownership,即多个线程共有这个数据。Rust 使用 Mutex
来保证 share memory 时的线程安全。
注意,Rust 中的 Mutex<T>
更像是 cpp 中的 atomic<T>
。cpp 中的std::mutex
是与被保护的数据独立的锁,而 Rust 中的Mutex<T>
owns 被保护数据,必须先 lock()
才能访问。
使用 Mutex 来保证线程独占式访问数据
Mutex 是 mutual exclusion 的缩写。Mutex 保证了在同一时间只有一个线程可以访问数据。它比较难用的地方在于:
- 使用数据前,需要加锁
- 使用数据后,需要解锁
下面是一个单线程例子,主要是讲 Mutex 的用法。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
// num is a MutexGuard
let mut num = m.lock().unwrap();
*num = 6;
// lock is released after MutexGuard goes out of scope
}
println!("m = {:?}", m);
}
Mutex<T>
是一个智能指针,对其调用 lock()
会返回一个 LockResult
,再调用 unwrap()
会有两种情况:
- 如果加锁成功,返回一个
MutexGuard
- 如果加锁失败,panic
MutexGuard<T>
类型,也就是上面代码中的 num
,也是一个智能指针。它的 Deref
, Drop
trait 分别实现为:
-
Deref
:返回T
的引用。因此*num = 6
将数字从 5 改成 6。 -
Drop
:释放锁。
多个线程共享一个 Mutex<T>
例如,用 10 个线程把数字 0 加到 10。
如果不考虑ownership,则会写出如下的错误代码:
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);
}
显然,由于我们使用了 move
closure,counter
已经被第一个线程拿走,后面的线程无法再使用。
因此,联想到智能指针,我们想要使用引用计数 Rc<T>
来实现共享。但是,Rc<T>
并不是线程安全的。好在 Rust 提供了一个线程安全版本的 Arc<T>
(atomically reference counted):
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 counter1 = Arc::clone(&counter); // ref count + 1 for each thread
let handle = thread::spawn(move || {
let mut num = counter1.lock().unwrap(); // move the "cloned" one
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result = {:?}", counter);
}
结果是:
Result = Mutex { data: 10 }
Send 和 Sync trait
Rust 并不从语言层面支持并发,我们目前见到的并发功能都是实现在标准库中。但是,有两个并发相关的概念是 Rust 语言支持的:std::marker::Send
和 std::marker::Sync
。
通过 Send 允许线程间传递 Ownership
所有实现了 Send
trait 的类型都能在线程间传递 ownership,但是,无论怎么传递,始终只有一个线程能够 own 这个数据。几乎所有的 Rust 基本数据类型都实现了 Send
。例如我们在给 spawn
传递 closure 时,经常加入 move
关键字来确保把变量的 ownership 交给了新创建的线程。
Rc<T>
是个例外,这是因为它持有的是底层变量的引用。如果它能传递给别的线程(例如传递 Rc::clone()
后的对象),则会发生多个线程同时更改某个变量的引用的 race condition。因此,在上面的例子中,使用 Rc<T>
会报错:
the trait Send is not implemented for Rc<Mutex<i32>>
我们将其替换为Arc<T>
就能通过编译,因为它实现了 Send
。
通过 Sync 允许多线程访问变量
实现了 Sync
trait 说明允许变量被多个线程同时“读取”。
一个类型
T
是Sync
当且仅当&T
是Send
。
这是由于,如果可以随意 clone 并且传递引用给别的线程,说明这个数据允许多个线程并发访问。注意,这里是 &T
,也就是说,可以“只读”访问也算是 Sync
。
因此,Rust 的大部分基本类型都是 Sync
。同样的, Rc<T>
是个例外,因为对 &Rc<T>
的拷贝会改变引用计数,所以 &Rc<T>
不是 Send
,从而 Rc<T>
也不是 Sync
。
网友评论