美文网首页
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

    除了 channel[https://www.jianshu.com/p/925d3534ac7f],我们也可以通...

  • Rust for cpp devs - 线程

    由于 Rust 特有的 ownership 和类型检查机制,许多并发问题都可以在编译期发现,这极大地降低了风险以及...

  • Rust for cpp devs - Ownership

    编程语言的内存管理一般有两种: 带垃圾回收机制的,如 Java,Golang,会在运行时检查不再使用的内存并回收,...

  • Rust for cpp devs - channel

    与 golang 一样,Rust 也实现了 channel 用于线程间的通信。如同 golang 的口号一样: D...

  • Rust for cpp devs - closure

    类似于 cpp 中的 lambda 表达式,Rust 中也有 closure。他们与普通函数的区别在于可以捕获变量...

  • Rust for cpp devs - Generic Type

    类似于 cpp,Rust 中也有泛型(generics),用于避免重复的代码。此外,还可以指定 traits 来限...

  • Rust for cpp devs - 迭代器

    迭代器(Iterator)可以允许对序列中的每一个元素执行某个操作。 Rust 的迭代器分为三种: iter() ...

  • Rust for cpp devs - minigrep 项目

    官方文档用 minigrep 项目来讲解如何组织一个 Rust 项目。 保持 main 函数简洁 这样做的好处是:...

  • Rust for cpp devs - 错误处理

    Rust 将软件中的错误分为两个类型:可恢复错误和不可恢复错误。 对于可恢复错误,例如文件找不到,可以报告给调用者...

  • Rust for cpp devs - 生命周期

    生命周期(lifetime)也是一类泛型。但它是用于指定引用保持有效的作用域。 Rust 中的每个引用都有相应的 ...

网友评论

      本文标题:Rust for cpp devs - mutex

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