最近在用 Rust 写多线程程序的时候,一个编译器的错误让我感到了困惑。
问题
简单来说,我有一个结构体,内部有一个 Rc
的值,如下
1
2
3
struct A {
val: Rc<u32>,
}
很显然,Rc
并不线程安全,所以加锁保护,并且用 Arc
在线程之间传递,所以写了如下的代码
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mutex = Mutex::new(A { val: Rc::new(5) });
let target = Arc::new(mutex);
let t = thread::spawn(move || {
target.lock();
// do something...
});
t.join().unwrap();
}
然而却遭到了编译器的无情吐槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error[E0277]: `std::rc::Rc<u32>` cannot be sent between threads safely
--> src/main.rs:13:13
|
13 | let t = thread::spawn(move || {
| ^^^^^^^^^^^^^ `std::rc::Rc<u32>` cannot be sent between threads safely
|
::: /home/zinglix/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:593:8
|
593 | F: Send + 'static,
| ---- required by this bound in `std::thread::spawn`
|
= help: within `A`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<u32>`
= note: required because it appears within the type `A`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Mutex<A>`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<std::sync::Mutex<A>>`
= note: required because it appears within the type `[closure@src/main.rs:13:27: 16:6 target:std::sync::Arc<std::sync::Mutex<A>>]`
简单来说就是,A
中的 Rc
没有实现 Send
,可这就让人很迷惑,我加锁就是为了让不线程安全的东西可以线程间共享,可它为什么还要我实现 Send
呢?
重新理解 Sync 与 Send
Sync
和 Send
是 Rust 多线程中十分重要的一个基础概念。
-
Sync
表示 在线程间共享 是安全的。 -
Send
表示 传递给另一个线程 是安全的。
Sync
说明不同线程可以同时使用同一个对象,例如同时读同一个常量。对于锁 Mutex
来说,因为几个线程可以同时对同一个锁执行 lock()
,所以满足 Sync
trait,只是锁的内部机制只会同时只允许一个线程获得锁,但这不重要,其对外的表现已满足要求。因此对于变量来说,通常套一层 Mutex
就可以让其可以在线程间共享。
Send
说明可以在线程间传递所有权,不同线程可以在不同时间使用同一个对象。线程 A 可以创建一个对象,然后传递 (send) 给线程 B,由于所有权机制,此后线程 B 就可以使用该对象而 A 不能,因为 A 中已将所有权传递给 B。不过如果对象实现了 Clone
,那么就可以拷贝一份到另一个线程之中。Mutex
并没有实现 Clone
trait,所以通常会使用 Arc<Mutex>
让多个线程同时拥有同一个锁。
值得一提的是,
Send
和Sync
是标记 trait,也就是说它们并不需要实现任何方法,一个结构体是否满足是手动标记的,而非编译器进行判断,除了通过所有成员都满足 trait 这一条件推断该结构体也满足 trait。正因此,手动实现这些 trait 通常涉及到 unsafe 代码,在实现时无法得到编译器的保护,需要小心实现以保证安全。
所以很容易看出,既然同一时间使用是安全的(Sync
),那么不同时间使用(Send
)也一定是安全,毕竟不能传递给其他线程,那怎么几个线程同时使用呢?所以几乎没有对象会是满足 Sync
而不满足 Send
。
回到问题
让我们去看一眼 Mutex
中的定义
1
2
3
4
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
可以看到 Mutex<T>
确实可以为所有类型 T
添加上 Sync
trait,但前提是 T
满足 Send
。为什么?因为 Mutex
想要实现 Sync
就肯定需要满足 Send
trait,那么其在传递时同样会传递 T
的所有权,因此 T
也需要满足 Send
。
那么回到问题,重新观察编译器提示的错误,根本问题是 Rc
不满足 Send
,所以 Mutex
不能实现 Send
,然后就产生一系列不满足的问题,最终导致不能在线程间传递。
为什么 Rc
不满足 Send
?Send
表示可以安全在线程间传递,然而对于 Rc
来说,如果我在当前线程有多个该 Rc
的拷贝,然后将一个传递给另一个线程,那么多个线程就都拥有了这个对象,然而 Rc
中的引用计数操作不是线程安全的,所以 Rc
不满足 Send
,这也就说明 Rc
在整个生命周期内都只能被一个线程拥有。
那用 Mutex<Rc>
加锁之后,就不会多个线程同时操作这个 Rc
,为什么也不行呢?表面上的原因在于 Mutex
要求 T
实现 Send
。更进一步,设想一下可以实现,那么完全可以在获得锁之后,复制一份 Rc
,然后将这份拷贝带出锁的作用范围,那么又可以几个线程同时修改引用计数了,你设想中的锁形同虚设,而 Rust 成功救你于水火之中。
嗯?你说
Mutex<Arc>
也会出这个问题?Arc
拷贝是线程安全的,并不会出问题。什么?你说复制一份然后就可以多线程同时修改了?从锁中复制一份
Arc
带出来,然而Arc
并不会给你 mut ref,并没有办法修改它。真想修改?再套个Mutex
吧
总结
根本问题出在 Rc
并不能安全地在线程间传递,所以换成 Mutex<Arc>
就可以了