在多线程中共享资源,除了原子类型之外,还可以考虑用锁来实现。在操作之前必须先获得锁,一把锁同时只能给一个线程,这样能保证同一时间只有一个线程能操作共享资源,操作完成后,再释放锁给等待的其他线程。在 Rust 中 std::sync::Mutex 就是一种锁。下面我们用 Mutex 来实现一下上面的原子类型的例子:

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

fn main() {
    let var : Arc<Mutex<u32>> = Arc::new(Mutex::new(5));
    let share_var = var.clone();

    // 创建一个新线程
    let new_thread = thread::spawn(move|| {
        let mut val = share_var.lock().unwrap();
        println!("share value in new thread: {}", *val);
        // 修改值
        *val = 9;
    });

    // 等待新建线程先执行
    new_thread.join().unwrap();
    println!("share value in main thread: {}", *(var.lock().unwrap()));
}

share value in new thread: 5
share value in main thread: 9

结果都一样,看来用 Mutex 也能实现,但如果从效率上比较,原子类型会更胜一筹。暂且不论这点,我们从代码里面看到,虽然有 lock,但是并么有看到有类似于 unlock 的代码出现,并不是不需要释放锁,而是 Rust 为了提高安全性,已然在 val 销毁的时候,自动释放锁了。同时我们发现,为了修改共享的值,开发者必须要调用 lock 才行,这样就又解决了一个安全问题。不得不再次赞叹一下 Rust 在多线程方面的安全性做得真是太好了。如果是其他语言,我们要做到安全,必然得自己来实现这些。

为了保障锁使用的安全性问题,Rust 做了很多工作,但从效率来看还不如原子类型,那么锁是否就没有存在的价值了?显然事实不可能是这样的,既然存在,那必然有其价值。它能解决原子类型锁不能解决的那百分之十的问题。我们再来看一下之前的一个例子:

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

fn main() {

    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair2 = pair.clone();

    // 创建一个新线程
    thread::spawn(move|| {
        let &(ref lock, ref cvar) = &*pair2;
        let mut started = lock.lock().unwrap();
        *started = true;
        cvar.notify_one();
        println!("notify main thread");
    });

    // 等待新线程先运行
    let &(ref lock, ref cvar) = &*pair;
    let mut started = lock.lock().unwrap();
    while !*started {
        println!("before wait");
        started = cvar.wait(started).unwrap();
        println!("after wait");
    }
}

代码中的 Condvar 就是条件变量,它提供了 wait 方法可以主动让当前线程等待,同时提供了 notify_one 方法,让其他线程唤醒正在等待的线程。这样就能完美实现顺序控制了。看起来好像条件变量把事都做完了,要 Mutex 干嘛呢?为了防止多个线程同时执行条件变量的 wait 操作,因为条件变量本身也是需要被保护的,这就是锁能做,而原子类型做不到的地方。

在 Rust 中,Mutex 是一种独占锁,同一时间只有一个线程能持有这个锁。这种锁会导致所有线程串行起来,这样虽然保证了安全,但效率并不高。对于写少读多的情况来说,如果在没有写的情况下,都是读取,那么应该是可以并发执行的,为了达到这个目的,几乎所有的编程语言都提供了一种叫读写锁的机制。

下一页