Published on

🦀 简单讲讲 Rust 多线程中的引用安全

Authors
  • avatar
    Name
    阿森 Hansen
    Twitter
  • 知识图谱位置:Rust-多线程
  • 阅读对象:对 Rust 感兴趣的工程师
  • 预备条件;Rust 所有权,Rust 闭包的基本概念

1 安全的变量:所有权和作用域是什么关系?

Rust 为了保证安全,要管理对变量的访问,而其他地方要访问这个变量,只能通过引用(有些地方也叫“借用”)。

变量的作用域就像一个个房子,一个变量只能存在于一个“房子”里,其他“房子”要使用这个变量,只能通过“引用”。例子中“作用域1”有变量的所有权,而“作用域2、3”只有变量的访问权,只能通过引用来访问

就像 Dota 中的幻影长矛手,放大招时只有一个本体,其他都是镜像。镜像就是对本体的引用。

满级的幻影长矛手,有多个“引用”,一个本体。(网图)

2 安全的闭包:闭包如何访问外部变量?

要创建一个线程,我们一般会这么写:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    // 创建一个线程
    let handle = thread::spawn(move || {  // 注意:这里使用了 move 关键字表示闭包要获取变量 v 的所有权!
        // 闭包的内部(一个作用域)。。。
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

我们知道,一个闭包包含了一个作用域。其中,spawn() 函数中的参数就是一个闭包,前面的 move 说明闭包捕获了 v 的所有权。从此以后,v 只能在闭包内部使用,闭包外面不能再使用了。

如果在闭包之后访问 v,编译器会报错。

2.1 闭包和外部作用域

首先介绍一下闭包。

闭包是一个轻量级的函数,而与函数不同的是,除了包含了一系列代码之外, 闭包还可以访问外部变量

闭包内部访问外部,就会有所有权问题。

现在,是时候拿出变量访问的三板斧1了。

而与之对应的,闭包访问变量也有三种方式:

  1. 闭包从外部捕获变量的所有权
  2. 从外部获取不可变引用
  3. 从外部获取可变引用

3 多线程的引用安全:为什么要 move ?

多线程应该选择哪一种呢?当然是最安全的那种!也就是第一个:“从外部获取所有权后访问”。

子线程就像是主线程生的孩子,而孩子长大了总是要独立的。孩子长大了结婚要自己搬出去住,谁也拦不住。一样地,子线程一旦生成就会脱离主线程的掌控

主线程生成一个子线程,但是主线程和子线程是独立运行的。各自的作用域也完全不同。

所以,Rust 为了保证最关键的“引用安全”,编译器会要求闭包函数捕获外部变量的所有权

不信你可以尝试拿去 move 关键字,Rust 编译立即报错。

4 编程时,应该注意什么?

所以,在写 Rust 时,一定要先设计好再实现,否则就可能会遭到编译器的拒绝编译暴击,注意以下两点:

  • 一定要想好哪些变量时要传给子线程的。因为,变量传递给子线程之后主线程就无法再访问了。
  • 如果主线程和子线程之间要传递数据,可以使用 Arc 原子引用计数、Mutex 互斥锁或者 mpsc 多发单收通道。

关于如何再主线程和子线程之间传递数据,具体细节本文不具体涉及,有兴趣可以自己查资料看看。有机会以后写写文章介绍一下。

5 总结

这篇文章介绍了 Rust 多线程的实现方式以及如何 Rust 是如何保证线程的引用安全的。希望能够帮到你。

参考

1 我在这篇文章中提出了“变量访问的三板斧”:🦀 从 Rust 的引用看计算机的内存和数据安全 | 阿森的知识图谱

使用多线程 - Rust语言圣经(Rust Course)

Using Threads to Run Code Simultaneously - The Rust Programming Language