【投稿】刀哥:Rust学习笔记 2
@[TOC](Rust 学习心得<2>:多线程)
现代的CPU
基本都是多核结构,为了充分利用多核的能力,多线程都是绕不开的话题。无论是同步或是异步编程,与多线程相关的问题一直都是困难并且容易出错的,本质上是因为多线程程序的复杂性,特别是竞争条件的错误,使得错误发生具备一定的随机性,而随着程序的规模越来越大,解决问题的难度也随之越来越高。
其他语言的做法
C/C++
将同步互斥,以及线程通信的问题全部交给了程序员。关键的共享资源一般需要通过Mutex/Semaphone/CondVariable之类的同步原语保证安全。简单地说,就是需要加锁。然而怎么加,在哪儿加,怎么释放,都是程序员的自由。不加也能跑,绝大多数时候,也不会出问题。当程序的负载上来之后,不经意间程序崩溃了,然后就是痛苦地寻找问题的过程。
Go
提供了通过channel
的消息机制来规范化协程之间的通信,但是对于共享资源,做法与C/C++
没有什么不同。当然,遇到的问题也是类似。
Rust 做法
与Go
类似,Rust
也提出了channel
机制用于线程之间的通信。因为Rust
所有权的关系,无法同时持有多个可变引用,因此channel
被分成了rx
和tx
两部分,使用起来没有Go
的那么直观和顺手。事实上,channel
的内部实现也是使用原子操作、同步原语对于共享资源的封装。所以,问题的根源依然在于Rust
如何操作共享资源。
Rust
通过所有权以及Type
系统给出了解决问题的一个不同的思路,共享资源的同步与互斥不再是程序员的选项,Rust
代码中同步及互斥相关的并发错误都是编译时错误,强迫程序员在开发时就写出正确的代码,这样远远好过面对在生产环境中顶着压力排查问题的窘境。我们来看一看这一切是如何做到的。
Send,Sync 究竟是什么
Rust
语言层面通过 std::marker
提供了 Send
和 Sync
两个Trait
。一般地说法,Send
标记表明类型的所有权可以在线程间传递,Sync
标记表明一个实现了Sync
的类型可以安全地在多个线程中拥有其值的引用。这段话很费解,为了更好地理解Send
和 Sync
,需要看一看这两个约束究竟是怎样被使用的。以下是标准库中std::thread::spawn()
的实现:
pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
unsafe { self.spawn_unchecked(f) }
}
可以看到,创建一个线程,需要提供一个闭包,而这个闭包的约束是 Send
,也就是需要能转移到线程中,闭包返回值T
的约束也是 Send
(这个不难理解,线程运行后返回值需要转移回去) 。举例说明,以下代码无法通过编译。
let a = Rc::new(100);
let h = thread::spawn(move|| {
let b = *a+1;
});
h.join();
编译器指出,std::rc::Rc<i32>
cannot be sent between threads safely。原因在于,闭包的实现在内部是由编译器创建一个匿名结构,将捕获的变量存入此结构。以上代码闭包大致被翻译成:
struct {
a: Rc::new(100),
...
}
而Rc<T>
是不支持 Send
的数据类型,因此该匿名结构,即这个闭包,也不支持 Send
,无法满足std::thread::spawn()
关于F
的约束。
上面代码改用Arc<T>
,则编译通过,因为Arc<T>
是一种支持 Send
的数据类型。但是Arc<T>
不允许共享可变引用,如果想实现多线程之间修改共享资源,则需要使用Mutex<T>
来包裹数据。代码会改为这个样子:
let mut a = Arc::new(Mutex::new(100));
let h = thread::spawn(move|| {
let mut shared = a.lock().unwrap();
*shared = 101;
});
h.join();
为什么Mutex<T>
可以做到这一点,能否改用RefCell<T>
完成相同功能?答案是否定的。我们来看一下这几个数据类型的限定:
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
unsafe impl<T: ?Sized> Send for RefCell<T> where T: Send {}
impl<T: ?Sized> !Sync for RefCell<T> {}
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
Arc<T>
可以Send
,当其包裹的T
同时支持Send
和Sync
。很明显Arc<RefCell<T>>
不满足此条件,因为RefCell<T>
不支持Sync
。而Mutex<T>
在其包裹的T
支持Send
的前提下,满足同时支持Send
和Sync
。实际上,Mutex<T>
的作用就是将一个支持Send
的普通数据结构转化为支持Sync
,进而可以通过Arc<T>
传入线程中。我们知道,多线程下访问共享资源需要加锁,所以Mutex::lock()
正是这样一个操作,lock()
之后便获取到内部数据的可变引用。
通过上述分析,我们看到Rust
另辟蹊径,利用所有权以及Type
系统在编译时刻解决了多线程共享资源的问题,的确是一个巧妙的设计。
异步代码,协程
异步代码同步互斥问题与同步多线程代码没有本质不同。异步运行库一般提供类似于std::thread::spawn()
的方式来创建协程/任务,以下是async-std
创建一个协程/任务的API
:
pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
Builder::new().spawn(future).expect("cannot spawn task")
}
可以看到,与std::thread::spawn()
非常相似,闭包换成了Future
,而Future
要求Send
约束。这意味着参数future
必须可以Send
。我们知道,async
语法通过generaror
生成了一个状态机驱动的Future
,而generaror
与闭包类似,捕获变量,放入一个匿名数据结构。所以这里变量必须也是Send
才能满足Future
的Send
约束条件。试图转移一个Rc<T>
进入async block
依然会被编译器拒绝。以下代码无法通过编译:
let a = Rc::new(100);
let h = task::spawn(async move {
let b = a;
});
此外,在异步代码中,原则上应当避免使用同步的操作从而影响异步代码的运行效率。试想一下,如果Future
中调用了std::mutex::lock
,则当前线程被挂起,Executor
将不再有机会执行其他任务。为此,异步运行库一般提供了类似于标准库的各种同步原语。这些同步原语不会挂起线程,而是当无法获取资源时返回Poll::Pending
,Executor
将当前任务挂起,执行其他任务。
完美了么?死锁问题
Rust
虽然用一种优雅的方式解决了多线程同步互斥的问题,但这并不能解决程序的逻辑错误。因此,多线程程序最令人头痛的死锁问题依然会存在于Rust
的代码中。所以说,所谓Rust
“无惧并发”是有前提的。至少在目前,看不到编译器可以智能到分析并解决人类逻辑错误的水平。当然,届时程序员这个岗位应该也就不存在了...
- 比特币项目
- HDU 1014 Uniform Generator【GCD,水】
- 【AlphaGo Zero 核心技术-深度强化学习教程代码实战05】SARSA(λ)算法实现
- 区块链应用场景:物联网和物流供应链
- HDU 1012 u Calculate e【暴力打表,水】
- Gym 100952C&&2015 HIAST Collegiate Programming Contest C. Palindrome Again !!【字符串,模拟】
- HDU 1013 Digital Roots【字符串,水】
- Gym 100952I&&2015 HIAST Collegiate Programming Contest I. Mancala【模拟】
- bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序
- Gym 100952E&&2015 HIAST Collegiate Programming Contest E. Arrange Teams【DFS+剪枝】
- Gym 100952H&&2015 HIAST Collegiate Programming Contest H. Special Palindrome【dp预处理+矩阵快速幂/打表解法】
- Gym 100952G&&2015 HIAST Collegiate Programming Contest G. The jar of divisors【简单博弈】
- Gym 100952F&&2015 HIAST Collegiate Programming Contest F. Contestants Ranking【BFS+STL乱搞(map+vector)+
- Gym 100952D&&2015 HIAST Collegiate Programming Contest D. Time to go back【杨辉三角预处理,组合数,dp】
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 632. 最小区间 Krains 2020-08-01 09:51:18 单调队列双指针堆
- 【R语言】高维数据可视化| ggplot2中会“分身术”的facet_wrap()与facet_grid()姐妹花
- 百万并发「零拷贝」技术系列之Java实现
- 腾讯云LiteAV、IM SDK(iOS)
- 在 Docker 中生成 ProtoBuffer、gRPC 文件
- Golang MongoDB Driver 更新符合条件的数组元素的字段
- 638. 大礼包 Krains 2020-08-01 19:48:29 动态规划DFS
- 650. 只有两个键的键盘 Krains 2020-08-02 09:39:39 动态规划DFS
- 类加载子系统 Krains 2020-07-31
- 理解类装载器
- MySQL-Python:使用技巧
- 使用Pytorch和Matplotlib可视化卷积神经网络的特征
- 微软复活20年前生产力工具PowerToys,填补Wind10缺失功能,开源且免费
- 运行时数据区 Krains 2020-08-01
- Docker 之容器间通信配置