在Rust中无畏并发(对比c++)
在程序中使用多线程非常困难, 我将演示如何通过使用Rust
来避免c++
中的一些缺陷,从而使多线程编程变的至少更容易一些。
记住一点: 复杂性不会消失只会转移。
在真实的代码库中,多线程编程通常更复杂,并且防止C++中的这些错误可能会更加困难。在Rust中,编译器仍将检查在那些复杂的情况下,提前阻止你犯下这些错误。
- 竞态条件
1 | cpp复制代码#include <thread> |
在C++
中,没有什么能阻止你引入竞争条件,并且在最糟糕的情况下甚至可以访问无效内存。
如果Thread1
在Thread2
循环期间更新数据data
,则Thread2
很容易访问已释放的数据data。
在比较大的CodeBases
中,即使使用std::atomic
或std::mutex
,也很难区分调用了哪些类的方法。
1 | rust复制代码fn main() { |
Rust
不会让我们这样做。
因为在Rust
中,任何变量都可以具有无限数量的不可变的引用,或单一的可变引用。
这意味着不会在安全的Rust
代码中出现这些竞态条件(race condition)的错误。
在Rust
中有很多方法可以修复上面的错误。
先看看互斥锁
和通道
。
1 | Rust复制代码use std::sync::{Arc, Mutex}; |
在上面这个例子中,使用了线程安全的引用计数指针(Arc)来存储数据,确保数据生命周期足够长。
如果只传参不可变数据,使用Arc
就足够了。
但是由于想要改变共享数据的状态,还需要将其包装在Rust
的std::sync::Mutex
中。
1 | Rust复制代码use std::{sync::mpsc::channel, thread}; |
对于在线程之间传递数据,可以使用通道(channel)来代替。
这些通道(channel)只允许发送线程安全的类型;如果试图发送一个线程不安全的类型跨线程使用(如Rc),
将会得到一个编译器错误。要想更快地实现多生产者多消费者模型,可以看下crossbeam_channel
.
2.生命周期和引用
1 | cpp复制代码#include <string> |
在这个例子中,生成的线程可能正在访问无效的内存,数据data
的析构函数在另外一个线程开始访问时可能早已经被调用。
现在的问题是: 我们需要确保在线程中使用的数据比线程本身活得更久。
在许多不同的场景中(不仅仅是多线程的场景),我们使用引用传递c++中的数据,而这些引用必须一直存在,直到引用的对方使用完它为止,而且没有自动的方法来检查引用是否存在。
1 | Rust复制代码fn main() { |
正如在第1部分的示例中看到的那样,编译器会抱怨,借来的变量生命周期不够长。
因为线程是独立运行的,所以这些线程中所指向的数据引用必须在整个程序运行期间都存在。
1 | Rust复制代码use std::sync::Arc; |
正如在前面的示例中看到的那样,可以使用Arc
来确保数据生命周期足够长。
1 | Rust复制代码use crossbeam_utils::thread; |
还可以使用来自crossbeam_utils
的范围线程。
其原理就是: 当派生的线程需要访问栈上的变量时,主动为其创建一个作用域。
3.处理来自线程的错误
1 | cpp复制代码#include <exception> |
上面的示例 我简单的写了下处理单个线程异常的最小代码,在c++
中处理线程的错误是相当复杂的。
1 | Rust复制代码fn main() { |
在Rust中,可以选择处理来自线程的panic
(或者只是调用.unwrap()
来终止,如果可以肯定线程永远不会恐慌的话),
并且可以返回Result<T, E>
来表示线程可能会失败。请注意,对于大多数线程,都不需要处理这两个中的任何一个。
4.Join() 和 detach()
1 | cpp复制代码#include <iostream> |
在c++中,如果忘记join
线程, 那么主线程会立马退出。
如果线程是joinable
状态的,那么线程(std::thread
)的析构函数将调用std::terminate
。
当线程调用析构函数的时候它可能还在运行。
1 | Rust复制代码fn main() { |
在Rust中,线程在其句柄被删除时隐式分离(运行到作用域外),因此不可能犯这种错误。(注意,你可能看不到打印Hello!
在主线程终止后)
当我没有在任何地方保存线程句柄时,我希望线程分离,否则我不会删除它。
c++将这种直观的行为视为不可恢复的运行时错误。
1 | cpp复制代码#include <iostream> |
试图join
一个分离的线程会导致崩溃。
1 | Rust复制代码fn main() { |
当作用域消失,自动删除线程句柄时线程被分离时,这个问题就不存在了。
- 引用传参
1 | cpp复制代码#include <iostream> |
即使在你不希望它用值传递参数的情况下,(std::thread
)也会用值传参(std::ref
)可以修复这个问题)。
1 | Rust复制代码fn main() { |
在Rust
中,你不能向线程传递参数,而是借用(borrow
)(使用scope
线程)或将变量移动(move
)到闭包中。
- 从线程中返回值
如果想在c++
中使用线程来正确地返回一个值,需要使用一些额外的同步机制。我这里给出两个最明显的答案: 通过引用或者std::future
。
1 | cpp复制代码#include <chrono> |
Rust
的线程提供了一种直接返回值的机制,这种操作内置在线程中。
1 | Rust复制代码fn main() { |
本文转载自: 掘金