分析tokio源码引发的可变借用 不可变借用 内部可变性与并发的关系思考 | rust 技术论坛-金年会app官方网

背景

今天在分析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)是一种设计模式,它允许你在通过不可变引用访问对象时修改其内部状态。这种模式通常通过类型系统中的某些类型来实现,如 refcellmutex。这些类型允许你在持有不可变引用的情况下进行修改。

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

但是,

问题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 或其他内部可变性类型是一种金年会app官方网的解决方案。这些类型允许在保持数据所有权和借用规则不变的前提下,实现对数据的安全并发修改。

具体示例

考虑以下代码示例,它展示了使用 &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 实现 send,前提是 t 也实现了 send
  • mutex 实现 sync,前提是 t 也实现了 sync

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

内部可变性

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

总结

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

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

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
11
粉丝
19
喜欢
99
收藏
22
排名:163
访问:5.9 万
博客标签
社区赞助商
网站地图