不知道你有没有好奇过,Rust
是怎么控制并发安全的。为什么编译器在编译时就能发现一些并发安全的问题。
今天拿例子聊聊这背后Rust
的两个并发约束trait
:Sync
和Send
,看看它们是怎么控制并发安全的。
Send
先来看看下边代码,尝试将String
类型的引用计数a
(Rc<String>
)移动到另一个线程中去,会发现编译器报错了。
1 | use std::{rc::Rc, thread}; |
仔细观察编译器的报错和下边相关代码trait
实现
1 | impl<T: ?Sized> !Send for Rc<T> {} |
你会发现thread::spawn
要求传入的闭包F
必须实现Send
,而Rc
类型的a
没有实现Send
,所以编译器报错了。
我们知道Rc
是引用计数,它为了性能没有实现原子操作的引用计数,如果在多个线程中共享,那么引用计数可能会出现计数错误,所以不能安全的跨线程共享。
那Send
是干什么的呢?
Send
是一个trait
,它标记了实现它的类型可以安全的在线程间传递所有权。也就是可以安全的移动(move
)其所有权。
Send trait
是一个标记型(marker)的trait
, 它没有实际方法,也不需要用户主动去实现,一般基本类型都实现了Send
。而复合类型如果包含的所有成员都实现了Send
,那么它也自动实现了Send
。(后面的Sync
也是这样的自动trait
)
也就是说,需要并发中需要安全传递值都需要被标记实现Send
,否则编译器会报错。
并发安全检查变成了trait bound
检查,这样就能在编译时发现问题,而不是在运行时,是不是很巧妙!
Sync
再来看看下边代码,尝试将String
类型的引用计数a
(&Rc<String>
)共享到一个线程中去,会发现编译器报错了。(注意没有移动,是共享)
1 | use std::{rc::Rc, thread}; |
自然,Rc
没有实现Sync
,所有编译器报错了。
1 | impl<T: ?Sized> !Sync for Rc<T> {} |
Sync
也是一个标记型trait
,它标记了实现它的类型可以安全的在线程间共享访问。
所谓共享,其实就是可以安全的引用。而如果&T
实现了Send
(可安全移动),那么T
就实现了Sync
(可安全共享其的引用)。
也就是说,需要并发中需要安全引用(&T
)都需要T
被标记实现了Sync
,否则编译器会报错。
又是一个巧妙的设计,通过trait bound
检查了引用是否满足并发安全。
总结一下:
Send
标记了实现它的类型可以安全的在线程间传递所有权(move
)。Sync
标记了实现它的类型可以安全的在线程间共享引用(&T
)。
最后推荐看看官方的这两篇文档来加深理解
如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com 本文链接 : https://newbmiao.github.io/2023/10/11/rust-send-and-sync-trait.html