分析tokio源码引发的可变借用+不可变借用+内部可变性与并发的关系思考

背景

今天在分析tokio =>tokio_test => src => task.rs中下面这段代码时,发现一个有趣的现象:

  impl ThreadWaker {
    fn new() -> Self {
        ThreadWaker {
            state: Mutex::new(IDLE),
            condvar: Condvar::new(),
        }
    }

    /// Clears any previously received wakes, avoiding potential spurious
    /// wake notifications. This should only be called immediately before running the
    /// task.
    fn clear(&self) {
        *self.state.lock().unwrap() = IDLE;
    }

clear方法中,对不可变借用的self可以改变里面的值,平时在用Mutex的时候只是在用,知道可以lock后可以修改其值,但是没有仔细去思考其中缘由。说到这里,其实我们会很自然的想到一个问题。

问题1: 既然想改变数据,为什么不明确传递&mut self呢?

Rust 中传递 &self&mut self 的区别在于所有权和借用的规则。下面我将详细解释为什么在某些情况下,即使想改变数据,也可能选择传递 &self 而不是 &mut self

&self vs &mut self

  • &self: 表示对 self 的不可变引用。通常用在不需要改变自身状态的方法中。
  • &mut self: 表示对 self 的可变引用。用于需要改变自身状态的方法中。

内部可变性(Interior Mutability)

在 Rust 中,内部可变性(Interior Mutability)是一种设计模式,它允许你在通过不可变引用访问对象时修改其内部状态。这种模式通常通过类型系统中的某些类型来实现,如 RefCell<T>Mutex<T>。这些类型允许你在持有不可变引用的情况下进行修改。

这就很好解释了为什么只需要不可变引用了。

但是,

问题2: 使用&self的好处是什么呢?

使用 &self 而不是 &mut self 修改数据的原因通常包括以下几点:

  1. 并发访问:
    • 当你需要在多线程环境中访问和修改数据时,使用 Mutex 是一个常见的模式。通过使用 Mutex,你可以确保即使多个线程同时访问数据,也能保持线程安全。而且你可以通过不可变引用 &self 来获取 Mutex 的锁并修改数据,而不需要可变引用。
  2. API 的灵活性:
    • 允许在方法中使用不可变引用 &self 提供了更大的灵活性。用户可以在持有对象的不可变引用时调用这些方法,而不需要担心可变借用的生命周期问题。
  3. 内部实现细节隐藏:
    • 使用内部可变性模式,你可以隐藏对象内部状态变化的细节,对外暴露一个更简洁的 API。例如,外部代码不需要知道你的对象内部使用了 MutexRefCell,只需要知道可以通过不可变引用来调用方法。

问题3: 那就是说,&mut其实本身强调同一时间只能有一个可修改的借用,用完后还回来其他地方才能继续可变借用走。这样就限制了并发吧?

是的,准确地说,&mut 确实强调同一时间只能有一个可修改的借用,这与 Rust 的所有权和借用规则密切相关。这些规则旨在确保内存安全和数据一致性。具体来说:

&mut 借用的特点

  • 独占性&mut 借用是独占的。在同一时间点,只能有一个可变引用。这意味着在持有 &mut 借用期间,不能有其他任何借用(无论是可变还是不可变)。
  • 可变性&mut 借用允许对数据进行修改。

这种设计的初衷是为了避免数据竞争和内存安全问题。在并发环境下,多个线程同时访问和修改同一数据可能导致数据竞争,而 Rust 通过严格的借用规则防止了这一点。

Mutex 和内部可变性

当需要在多线程环境中安全地修改数据时,使用 Mutex 或其他内部可变性类型是一种解决方案。这些类型允许在保持数据所有权和借用规则不变的前提下,实现对数据的安全并发修改。

具体示例

考虑以下代码示例,它展示了使用 &mutMutex 的区别:

使用 &mut

struct Data {
    value: i32,
}

impl Data {
    fn set_value(&mut self, new_value: i32) {
        self.value = new_value;
    }
}

fn main() {
    let mut data = Data { value: 0 };
    data.set_value(42);
}

在这个示例中,set_value 方法需要 &mut self,这意味着在调用 set_value 方法期间,data 不能有其他借用。这限制了数据在并发场景中的修改,因为不能同时有多个线程持有 &mut 借用。

使用 Mutex

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

struct Data {
    value: Mutex<i32>,
}

impl Data {
    fn set_value(&self, new_value: i32) {
        let mut value = self.value.lock().unwrap();
        *value = new_value;
    }
}

fn main() {
    let data = Arc::new(Data { value: Mutex::new(0) });

    let mut handles = vec![];
    for i in 0..10 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            data.set_value(i);
        });
        handles.push(handle);
    }

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

    println!("Final value: {:?}", data.value.lock().unwrap());
}

在这个并发示例中,多个线程可以同时调用 set_value 方法,因为每个线程都会获取 Mutex 的锁,从而保证数据修改的安全性和一致性。

那么问题又来了

问题3: Mutex 怎么保证线程间和线程内部数据安全和内部可变性的呢?

Mutex 通过实现 SendSync 这两个 trait 来保证在线程之间安全地传递和共享数据,从而实现内部可变性。下面是更详细的解释:

SendSync Trait

  • Send: 允许类型的所有权在线程间转移。如果一个类型实现了 Send,那么它的实例可以安全地在线程间移动。
  • Sync: 允许类型的引用在多个线程中共享。如果一个类型实现了 Sync,那么对该类型的引用可以安全地在多个线程中共享。

Mutex 的实现

在 Rust 标准库中,Mutex 实现了 SendSync,确保其在线程之间传递和共享时的安全性:

  • Mutex<T> 实现 Send,前提是 T 也实现了 Send
  • Mutex<T> 实现 Sync,前提是 T 也实现了 Sync

通过实现这两个 trait,Mutex 可以在线程间安全地传递,并允许多个线程安全地访问和修改被保护的数据。

内部可变性

由于 Mutex 提供了内部可变性,即使通过不可变引用 &self,也可以安全地修改内部的数据。这是通过锁机制实现的,确保同一时间只有一个线程可以访问被保护的数据,从而避免数据竞争和不一致性。

总结

其实,这种设计模式在 Rust 中非常常见,特别是在需要内部可变性和线程安全的场景下使用 MutexRwLock 等类型。

本作品采用《CC 协议》,转载必须注明作者和本文链接
努力是不会骗人的!
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!