并发主题

Rust语言并发模型

  Rust语言项目初始是为了解决两个棘手问题:

1. 如何进行安全的系统编程?

2.如何实现无痛苦的并发编程

最初,这些问题似乎是正交的不相关,但是让我们惊讶的是,最终解决方案被证明是相同的:同样使Rust安全的工具也帮助你正面解决并发。

内存的安全错误和并发错误往往归结为代码访问数据引起的问题,这是不应该的。Rust秘密武器是ownership,系统程序员需要服从的访问控制纪律,Rust编译器也会为你静态地检查。

对于内存安全,意味着你在一个没有垃圾回收机制下编程,不用害怕segfault,因为Rust会抓住这些错误。

对于并发,这意味着你可以选择各种各样的并发范式(消息传递、共享状态、无锁、纯函数式),而Rust会帮助你避免常见的陷阱。

下面是Rust的并发风格:

  •  channel只传送属于其的消息,你能从一个线程发送指针到另外一个线程,而不用担心这两个线程因为同时访问这个指针产生竞争争夺,Rust的channel通道是线程隔离的。

  • lock知道其保护哦数据,当一个锁被一个线程hold住,Rust确保数据只能被这个线程访问,状态从来不会意外地被分享,"锁住数据,而不是代码" 是Rust特点

  • 每个数据类型都能知晓其是否可以在多线程之间安全传输或访问,Rust增强这种安全用途;也就没有数据访问争夺,即使对于无锁的数据结构,线程安全不只是文档上写写,而是其实在的法律规则。

  • 你能在线程之间分享stack frames , Rust会确保这个frame在其他线程还在使用它时一直活跃,在Rust中即使最大胆的共享也会确保安全。

所有这些好处都是得益于Rust的所有权模型,和事实上锁、通道channel和无锁数据结构等之类的库包,这意味着Rust的并发目标是开放的,新的库包对新的范式编程更有力,也能捕获更多bug,这些都只要使用Rust的所有权特性来增加拓展API。

 

背景:所有权ownership

  在Rust中,每个值都有一个所有作用域(owning scope),传送或返回一个值意味着传送ownership所有权到一个新的作用域。当作用域结束自动销毁时,值还是被拥有的。

  让我们看看简单案例,假设我们创建一个vector,放入一些元素:

fn make_vec() {

    let mut vec = Vec::new(); // owned by make_vec's scope

    vec.push(0);

    vec.push(1);

    // scope ends, `vec` is destroyed

}

这个作用域创建一个值并开始拥有它,make_vec的整个部分是vec的拥有作用域,拥有者能使用vec做任何事情,包括改变它,作用域结束后,也就是到该方法结束处,vec还是被拥有,直到其自动被释放。

如果这个vector返回或被传送时更有趣:

fn make_vec() -> Vec<i32> {

    let mut vec = Vec::new();

    vec.push(0);

    vec.push(1);

    vec // 传送所有权给调用者

}

 

fn print_vec(vec: Vec<i32>) {

    // `vec`参数是这个作用域一部分, 这样它被`print_vec`拥有

 

    for i in vec.iter() {

        println!("{}", i)

    }

 

    // 现在, `vec` 被释放

}

 

fn use_vec() {

    let vec = make_vec(); // 获得vector拥有权

    print_vec(vec);       // 将拥有权传给 `print_vec`

}

现在,在make_vec作用域结束之前,vec通过被返回移出了原来的作用域,它并没有被销毁,一个调用者如use_vec然后会接受vector的拥有权。

另外一方面,print_vec函数将vec作为输入参数,vector拥有权被传送到它的调用者,因为print_vec并不再传送拥有权了,在其作用域结束后vector会被销毁。

一旦拥有权被交出了,其值就再也不能被使用,比如考虑下面use_vec变量:

fn use_vec() {

    let vec = make_vec();  // 获得vector拥有权

    print_vec(vec);        // 传送拥有权给 `print_vec`

 

    for i in vec.iter() {  // 继续使用`vec`

        println!("{}", i * 2)

    }

}

编译这段代码会得到错误提示:

error: use of moved value: `vec`

for i in vec.iter() {
^~~

编译器认为vec不能再被使用,因为其所有权已经被传递给到其他地方。灾难得以避免。

 

背景:borrowing出借

  到目前整个故事还不是很完美,因为将vector完全交给print_vec并不是我们真正意图,我们只是授权print_vec临时访问这个vector,然后我们还想继续使用vector,这种情况也是常常发生。

  那么borrowing这个概念就来了,如果你得访问一个值,你能将其出租给你调用的函数,Rust会检查这些租约会不会超过被借用的对象。为了借用一个值,你得引用它,也就是某个指针,使用 &操作符:

fn print_vec(vec: &Vec<i32>) {

    // `vec` 参数被借用到这个作用域

 

    for i in vec.iter() {

        println!("{}", i)

    }

 

    // 借用结束

}

 

fn use_vec() {

    let vec = make_vec();  // 获得vector拥有权

    print_vec(&vec);       // 出租访问权给`print_vec`

    for i in vec.iter() {  // 继续使用 `vec`

        println!("{}", i * 2)

    }

    // vec在这里被释放

}

这里实现了vector临时出租情况。

每个引用对于有限的作用域是有效的,编译器会自动决定,引用一般有下面两个风格:

  • 不可变引用 &T, 允许分享但是不能改变,能有多个引用 &T 同时指向同一个值,但是这些引用如果在活跃情况下(通过线程)是不能改变这个值。

  • 可变引用 &mut T, 允许改变,但不能被分享,如果有一个可变引用&mut T 指向值,就不能同时有其他活跃的引用指向这个值,这个值可以被改变。

Rust会在编译时检查这些规则,borrowing并没有运行额外负担。

 

消息传递

  前面我们已经具备了Rust的背景知识,现在可以看看这些概念对于Rust的并发模型意味着什么?

  并发编程有很多风格,但是特别简单的一个是消息传递,线程或actor会通过彼此发送消息通讯,这种风格是强调一起共享和通讯。不是通过共享内存进行通讯,而是通过通讯消息传递实现内存的共享。

  Rust的拥有权使得其在编译时就能够方便发现问题,考虑下面的channel API:

fn send<T: Send>(chan: &Channel<T>, t: T);

fn recv<T: Send>(chan: &Channel<T>) -> T;

  Channel是基于数据类型的泛型,它们会转化,Send部分代码意味着T被认为安全地在线程之间发送。

  在Rust中,传送一个T给send函数意味着传送拥有权给它,这具有深远的影响:它意味着像下面的代码会产生一个编译错误:

/ Suppose chan: Channel<Vec<i32>>

 

let mut vec = Vec::new();

// do some computation

send(&chan, vec);

print_vec(&vec);

  在这里,线程创建一个vector,将其发送到另外一个线程,然后继续使用它,这个线程接受到vector会改变它然后继续运行,这样调用print_vec会导致两个线程竞争情况,这是一个use-after-free bug

  这里,Rust编译器会产生一个错误警告:

Error: use of moved value `vec`

 

  另外一个处理并发的方式是通过共享锁进行线程通讯。共享状态的并发有坏名声,它一般会让人忘记去加锁,或者在错误时间实现了自己都不知道的错误数据,带来灾难后果。Rust是这样实现:

  • 共享状态并发仍然是一个基本编程风格,特别对于为了最大性能的系统编码。

  • 真正问题是不小心共享了状态。

Rust瞄准了提供给你征服共享状态并发的直接攻击,无论你是否使用锁或无锁技术。

在Rust中,线程是彼此自动隔离的,这是因为所有权。那么写只会在这个线程能够访问时才会发生,访问方式是或者拥有被访问的数据拥有权,或者有一个可变的数据出借权,无论哪个方式,线程只会在某个时刻只有一个访问数据。

记住:可变的出借borrow不会与其他出借同时发生,锁也提供同样的授权,也就是可变的排他,这是通过在运行时的同步实现的,锁的API其实内部有一个钩子直接通往Rust的拥有权系统。比如下面代码:

// 创建一个新的 mutex

fn mutex<T: Send>(t: T) -> Mutex<T>;

 

// 获得锁

fn lock<T: Send>(mutex: &Mutex<T>) -> MutexGuard<T>;

 

// 通过锁保护数据访问

fn access<T: Send>(guard: &mut MutexGuard<T>) -> &mut T;

锁的API有几个不寻常的特点:

首先,Mutex类似是基于T类型的泛型,而T是被锁保护的数据的类型,当你创建一个Mutex,你能传递这个数据的拥有权给mutex,然后会立即放弃再次访问它的能力(锁首次被创建时是出于解锁状态)。

然后,你能用lock这段代码堵塞线程知道锁被获得,这个函数也不寻常提供一个返回值MutexGuard<T>,这个MutexGuard在其被销毁时会自动释放锁,不会有单独的unlock函数来做这件事。

访问锁只有一个途径是通过access 函数,它会将一个guard的可变性borrow转为数据的可变borrow:

fn use_lock(mutex: &Mutex<Vec<i32>>) {

    // 获得锁,取得guard的拥有权;

    // 锁将被当前作用域一直持有

    let mut guard = lock(mutex);

 

    // 通过可变的出借guard来访问数据

    let vec = access(&mut guard);

 

    // vec 有类型 `&mut Vec<i32>`

    vec.push(3);

 

    // 当`guard`被销毁时,这里锁自动释放

}

这里有两个关键组成:

  • 通过 access返回的可变引用生存周期不能超过借用的 MutexGuard 

  • 锁只会在 MutexGuard 被摧毁时释放。

这个结果是Rust有力的锁纪律:当有锁把持数据时,它不会让你有额外其他能力访问被锁保护的数据。所有其他的试图访问都会产生编译错误:

fn use_lock(mutex: &Mutex<Vec<i32>>) {

    let vec = {

        // 获得锁

        let mut guard = lock(mutex);

 

        // 试图返回数据的出借borrow

        access(&mut guard)

 

        // guard被销毁,锁释放

    };

 

    // 在锁外部试图访问数据

    vec.push(3);

}

Rust会产生一个编译错误:

error: `guard` does not live long enough
access(&mut guard)
^~~~~

 

线程安全和“Send”

  线程安全的数据结构使用内部同步机制能够被多线程同时并发安全地访问,Rust提供两种聪明的指针为引用计数:

  • Rc<T> 提供为正常读/写提供引用计数,不是线程安全

  • Arc<T> 为原子操作提供引用计数,它是线程安全的。

通过使用Arc的硬件原子操作会被通过Rc的香草性轻量操作有代价成本,这样,使用Rc而不是Arc是有好处的,另外一个方面,Rc<T>从来不会从一个线程迁移到另外一个也是很关键,因为会导致竞争中断计数。

大多数语言在线程安全与线程不安全之间没有语法区分,靠的是详细文档。

在Rust中,世界被划分为两种数据类型,Send意味着它们能被安全地从一个线程移到另外一个线程,而!Send意味着这么做是不安全的,如果一个类型的组件都是Send,那么那就是线程安全的,当然基本类型不会继承线程安全,正常情况下:Arc是Send,而Rc不是。

我们已经看到Channel和Mutex只和带有Send数据工作,因为它们跨线程访问数据的边界,它们也是为Send强有力的支持特性。

 

Rust语言介绍