Rust 进阶教程——读写锁

2023年 8月 10日 35.8k 0

Rust 进阶教程——读写锁

0x00 开篇

大家好久不见,最近一段时间工作比较繁忙,公众号停更了一段时间,接下来应该会回归正常,感谢大家的不离不弃!!

上一篇文章介绍了 Mutex(互斥量/互斥锁),但是它不能区分获取锁的读者或者写者,因此会阻塞任何等待锁变为可用的线程。本篇文章将介绍在多线程编程中的另一个概念——读写锁。读写锁在没有写者持有锁时,允许任意数量的读者获取锁。本篇文章阅读时长 5 分钟。

0x01 定义

读写锁是一种同步机制,可在多线程环境中控制共享数据的访问。读写锁允许多个线程同时读取数据,但只允许一个线程写入数据,在 Rust 中使用 RwLock 来操作读写锁。对于这种锁,写入部分通常允许对底层数据进行修改(独占访问),而读取部分通常只允许进行只读访问(共享访问)。读写锁的优先级策略取决于底层操作系统的实现,并且该类型不保证使用任何特定的策略。

先来看一个示例:

fn main() {
    let rust = "hello rust!".to_string();
    let lock = RwLock::new(rust);
    {
        let read1 = lock.read().unwrap();
        let read2 = lock.read().unwrap();
        println!("read1: {}, read2: {}", read1, read2);
    } // 读锁在这里被释放

    // 仅能持有一个写锁
    {
        let mut write = lock.write().unwrap();
        (*write).push_str(" hello world!");
        println!("write: {}", *write);
    } // 写锁在这里被释放
}
//

首先,创建一个不可变的字符串 hello rust!,然后通过 RwLock::new 创建一个包含字符串 "hello rust!" 的 RwLock 对象 lock。在第一个代码块内部,我们获取了两个读锁(read1 和 read2 ),同时对数据进行只读访问。在第二个代码块内部,我们获取了一个写锁来修改数据。写锁是独占的,只能有一个线程或进程持有。然后将 lock.write() 返回的结果作为可变引用来向字符串追加文本 " hello world!"

还有需要注意的一点,在第一个代码块和第二个代码块之间,读锁被释放。这是因为 RwLock 允许多个线程同时持有读锁,但是在持有写锁时,读锁将会被阻塞。这保证了在有写锁时,不会有并发的读操作。最后,随着第二个代码块的结束,写锁也被释放。

0x02 多线程中使用读写锁

下面是一个在多线程中使用读写锁的示例:

fn main() {
    // 使用`Arc`和`RwLock`创建一个共享数据结构
    let shared_data = Arc::new(RwLock::new(0));

    // 创建读线程
    let reader_shared_data = shared_data.clone();
    thread::spawn(move || {
        // 获取读锁
        let reader = reader_shared_data.read().unwrap();

        // 在共享数据上执行只读操作
        println!("读取: {}", *reader);

        // 当`reader`离开作用域时,读锁会被释放
    }).join().unwrap();

    // 创建一个写线程
    let writer_data = shared_data.clone();
    thread::spawn(move || {
        // 获取写锁
        let mut writer = writer_data.write().unwrap();

        // 在共享数据上执行写操作
        *writer += 1;
        println!("写入: {}", *writer);
        thread::sleep(Duration::from_secs(2));
        // 当`writer`离开作用域时,写锁会被释放
    }).join().unwrap();


    // 再次创建读线程
    thread::spawn(move || {
        // 获取读锁
        let reader = shared_data.read().unwrap();

        // 在共享数据上执行只读操作
        println!("读取: {}", *reader);

        // 当`reader`离开作用域时,读锁会被释放
    }).join().unwrap();
}

在使用 Rust 读写锁时,最好将锁封装到 Arc(原子引用计数)中。这样可以确保多个线程可以共享读写锁的所有权。

首先,通过Arcclone方法创建了两个共享数据的副本,分别用于读线程和写线程。Arc(原子引用计数)是一种多线程安全的引用计数智能指针,可以在多个线程之间共享数据所有权。

然后,创建了读线程,并使用 thread::spawn 方法和 move 关键字传递了一个闭包作为线程的执行体。闭包内部首先获取了读锁,然后对共享数据执行只读操作(这里是简单地打印出共享数据的值)。最后,当闭包结束时,读锁会被自动释放。join 方法用于等待线程的执行结束。

随后,创建了写线程,其过程类似于上面的读线程。写线程获取写锁后,对共享数据执行写操作(这里是将共享数据加1并打印出结果),并在之后睡眠2秒,模拟执行写操作需要的时间。当写线程结束时,写锁会被自动释放。

最后,再次创建了一个读线程,并重复之前的读操作。这次读线程会读取之前写线程修改后的共享数据。

整个程序通过读写锁实现多线程对共享数据的并发访问和操作,保证读操作之间不会发生冲突,而写操作和读操作之间互斥。这样可以同时进行读操作,而在写操作时保证其他线程无法读取或写入数据,从而避免了数据竞争和并发访问的问题。

0x03 RwLock 源码解读

一起来看下 RwLock 的源码:Rust 进阶教程——读写锁

RwLock 结构体有三个字段:

  • inner:表示底层的操作系统级别的读写锁实现。它是一个 sys::RwLock 类型的变量,该类型是平台相关的,即每个平台都有自己的实现方式。

  • poison:这个字段在上一篇文章的 Mutex 已经认识过它,是一个标记字段。用于标记读写锁是否处于"中毒"状态,即被线程异常终止或出现了某些错误。

  • data:是 T 类型数据的包装器,其中包含一个 UnsafeCell 类型的变量。通过 UnsafeCell 可以在并发环境下安全地进行内部可变性。

    PS: 在第一个示例中,我们创建了一个不可变的字符串 hello rust!,但是当我们获取读写锁的时候可以修改它,这里就是通过 UnsafeCell 实现的,这也是前面所说的 Rust 的内部修改能力。

0x04 扩展阅读: UnsafeCell 和RefCell 、Cell的区别

上面提到了 UnsafeCell,那这与前面介绍的 RefCell 和Cell 有什么区别呢?

UnsafeCell : UnsafeCell 是 Rust 内部可变性的核心原语是,它是一种提供了对数据进行内部可变操作的安全抽象的类型,可以进行更低级别的内存操作。UnsafeCell 本身是一个零成本的抽象,它的存在不会引入任何开销,只是提供了一个原始的可变性接口。使用 UnsafeCell 时,程序员需要自行保证线程安全性。使用 UnsafeCell 需要特别小心,因为它可能导致数据竞争和未定义行为。

RefCell 、Cell: 它们内部对 UnsafeCell 进行了包裹,它们提供了一种在单线程环境下进行内部可变性的方式。通过使用内部可变性模式,允许在不借用整个结构体的情况下,在不可变引用或可变引用之间切换。它们内部在运行时进行了借用检查,而不是在编译时进行,这使得在运行时进行更灵活的借用规则检查。

总的来讲就是,UnsafeCell 提供了一种更低级别的原始内部可变性,需要程序员自己确保线程安全;而 RefCell 和 Cell 则提供了一个安全的运行时内部可变性抽象,它在运行时借用检查的帮助下实现了线程安全。

0x05 小结

RwLock(读写锁)是一种并发原语,用于保护共享数据的并发访问。在多线程环境中,允许多个线程同时读取共享数据,但只允许一个线程写入数据。

RwLock 保证了以下关键点:

  • 写锁具有互斥性,当写锁被持有时,其他线程无法同时持有写锁或读锁,确保了数据的一致性。
  • 读锁与其他读锁之间并发无阻塞,可以同时持有多个读锁,提高了读操作的并发性能。
  • 读锁与写锁之间具有互斥性,当读锁被持有时,其他线程无法同时持有写锁,保证了写操作的独占性。

总的来讲,RwLock 在多线程环境中提供了一种简单、安全和高效的方式来保护共享资源的并发访问。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论