借用检查器并共享 I/O

Borrow checker and shared I/O

我在我的代码中遇到了一个问题,多个结构需要将数据发送到一个共享的输出接收器,而借用检查器不喜欢它。

struct SharedWriter {
    count: u32,
}

impl SharedWriter {
    pub fn write(&mut self) {
        self.count += 1;
    }
}

struct Container<'a> {
    writer: &'a mut SharedWriter,
}

impl Container<'_> {
    pub fn write(&mut self) {
        self.writer.write();
    }
}

pub fn test() {
    let mut writer = SharedWriter { count: 0 };

    let mut c0 = Container {
        writer: &mut writer,
    };

    let mut c1 = Container {
        // compiler chokes here with:
        // cannot borrow `writer` as mutable more than once at a time
        writer: &mut writer,
    };

    c0.write();
    c1.write();
}

我了解问题及其发生的原因;你不能一次多次借用可变的东西。

我不明白的是一个很好的通用解决方案。这种模式经常发生。您有一个通用的输出接收器,如文件、套接字或数据库,并且您想要向其提供多个数据流。如果它保持任何状态,它就必须是可变的。如果它拥有任何资源,它必须只是一个单一的实体。

您可以在每个单独的 write() 方法 (write(&mut writer, some_data)) 中传递对接收器的引用,但这会使代码混乱并且每秒将被调用(在我的特定应用程序中)数百万次。我推测一遍又一遍地传递这个参数会有一些额外的开销。

是否有一些语法可以解决这个问题?

Interior mutability.

对于您的情况,最简单的方法可能是使用 RefCell。它会有一些运行时开销,但它是安全的。

use std::cell::RefCell;

struct SharedWriter {
    count: RefCell<u32>,
}

impl SharedWriter {
    pub fn new(count: u32) -> Self {
        Self { count: RefCell::new(count) }
    }
    
    pub fn write(&self) {
        *self.count.borrow_mut() += 1;
    }
}

如果数据是 Copy(如 u32,以防这是您的真实数据),您可能需要使用 Cell。适用类型较少但zero-cost:

use std::cell::Cell;

struct SharedWriter {
    count: Cell<u32>,
}

impl SharedWriter {
    pub fn new(count: u32) -> Self {
        Self { count: Cell::new(count) }
    }
    
    pub fn write(&self) {
        self.count.set(self.count.get() + 1);
    }
}

有更多内部可变性原语(例如,UnsafeCell 用于 zero-cost 但不安全访问,或用于线程安全突变的互斥锁和原子)。

一种选择是使用频道。这是一个看起来如何的例子。这还有一个额外的好处,那就是允许您使用 io.js 跨多个线程进行扩展。它需要一个 handler ,它在新线程上循环运行。它会阻塞,直到收到通过发送方发送的值,然后使用给定值和对处理程序的可变引用调用 func。当所有发送者都被删除时,线程退出。然而,这种方法的一个缺点是通道只能在一个方向上工作。

use std::sync::mpsc::{channel, Sender};
use std::thread::{self, JoinHandle};

pub fn create_shared_io<T, H, F>(mut handler: H, mut func: F) -> (JoinHandle<H>, Sender<T>)
where
    T: 'static + Send,
    H: 'static + Send,
    F: 'static + FnMut(&mut H, T) + Send,
{
    let (send, recv) = channel();

    let join_handle = thread::spawn(move || loop {
        let value = match recv.recv() {
            Ok(v) => v,
            Err(_) => break handler,
        };

        func(&mut handler, value);
    });

    (join_handle, send)
}

然后就可以像你的例子一样使用了。由于您的示例中未传递任何数据,因此它发送 () 作为占位符。

pub fn main() {
    let writer = SharedWriter { count: 0 };
    println!("Starting!");
    
    let (join_handle, sender) = create_shared_io(writer, |writer, _| {
        writer.count += 1;
        println!("Current count: {}", writer.count);
    });

    let mut c0 = Container {
        writer: sender.clone(),
    };

    let mut c1 = Container {
        writer: sender,
    };

    c0.write();
    c1.write();
    
    // Ensure the senders are dropped before we join the io thread to avoid possible deadlock
    // where the compiler attempts to drop these values after the join.
    std::mem::drop((c0, c1));
    
    // Writer is returned when the thread is joined
    let writer = join_handle.join().unwrap();
    println!("Finished!");
}

struct SharedWriter {
    count: u32,
}

struct Container {
    writer: Sender<()>,
}

impl Container {
    pub fn write(&mut self) {
        self.writer.send(()).unwrap();
    }
}