Rust for cpp devs - mutex

除了 channel,我们也可以通过share memory 来进行多线程通信,它的特点是 multiple ownership,即多个线程共有这个数据。Rust 使用 Mutex 来保证 share memory 时的线程安全。

注意,Rust 中的 Mutex<T> 更像是 cpp 中的 atomic<T>。cpp 中的std::mutex 是与被保护的数据独立的锁,而 Rust 中的Mutex<T> owns 被保护数据,必须先 lock() 才能访问。

使用 Mutex 来保证线程独占式访问数据

Mutex 是 mutual exclusion 的缩写。Mutex 保证了在同一时间只有一个线程可以访问数据。它比较难用的地方在于:

  • 使用数据前,需要加锁
  • 使用数据后,需要解锁

下面是一个单线程例子,主要是讲 Mutex 的用法。

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);
    {
        // num is a MutexGuard
        let mut num = m.lock().unwrap();
        *num = 6;
        // lock is released after MutexGuard goes out of scope
    }
    println!("m = {:?}", m);
}

Mutex<T> 是一个智能指针,对其调用 lock() 会返回一个 LockResult,再调用 unwrap() 会有两种情况:

  • 如果加锁成功,返回一个 MutexGuard
  • 如果加锁失败,panic

MutexGuard<T> 类型,也就是上面代码中的 num,也是一个智能指针。它的 DerefDrop trait 分别实现为:

  • Deref:返回 T 的引用。因此 *num = 6 将数字从 5 改成 6。
  • Drop:释放锁。

多个线程共享一个 Mutex<T>

例如,用 10 个线程把数字 0 加到 10。

如果不考虑ownership,则会写出如下的错误代码:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);

    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result = {:?}", counter);
}

显然,由于我们使用了 move closure,counter 已经被第一个线程拿走,后面的线程无法再使用。

因此,联想到智能指针,我们想要使用引用计数 Rc<T> 来实现共享。但是,Rc<T> 并不是线程安全的。好在 Rust 提供了一个线程安全版本的 Arc<T> (atomically reference counted):

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));

    let mut handles = vec![];

    for _ in 0..10 {
        let counter1 = Arc::clone(&counter);  // ref count + 1 for each thread
        let handle = thread::spawn(move || {
            let mut num = counter1.lock().unwrap();  // move the "cloned" one
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result = {:?}", counter);
}

结果是:

Result = Mutex { data: 10 }

Send 和 Sync trait

Rust 并不从语言层面支持并发,我们目前见到的并发功能都是实现在标准库中。但是,有两个并发相关的概念是 Rust 语言支持的:std::marker::Sendstd::marker::Sync

通过 Send 允许线程间传递 Ownership

所有实现了 Send trait 的类型都能在线程间传递 ownership,但是,无论怎么传递,始终只有一个线程能够 own 这个数据。几乎所有的 Rust 基本数据类型都实现了 Send。例如我们在给 spawn 传递 closure 时,经常加入 move 关键字来确保把变量的 ownership 交给了新创建的线程。

Rc<T> 是个例外,这是因为它持有的是底层变量的引用。如果它能传递给别的线程(例如传递 Rc::clone() 后的对象),则会发生多个线程同时更改某个变量的引用的 race condition。因此,在上面的例子中,使用 Rc<T> 会报错:

the trait Send is not implemented for Rc<Mutex<i32>>

我们将其替换为Arc<T> 就能通过编译,因为它实现了 Send

通过 Sync 允许多线程访问变量

实现了 Sync trait 说明允许变量被多个线程同时“读取”。

一个类型 TSync 当且仅当 &TSend

这是由于,如果可以随意 clone 并且传递引用给别的线程,说明这个数据允许多个线程并发访问。注意,这里是 &T,也就是说,可以“只读”访问也算是 Sync

因此,Rust 的大部分基本类型都是 Sync。同样的, Rc<T> 是个例外,因为对 &Rc<T> 的拷贝会改变引用计数,所以 &Rc<T> 不是 Send,从而 Rc<T> 也不是 Sync

推荐阅读更多精彩内容