美文网首页
Rust for cpp devs - mutex

Rust for cpp devs - mutex

作者: 找不到工作 | 来源:发表于2021-05-22 20:59 被阅读0次

    除了 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

    相关文章

      网友评论

          本文标题:Rust for cpp devs - mutex

          本文链接:https://www.haomeiwen.com/subject/qqrojltx.html