Search code examples
rustownership

How to implement Reader-Writer pattern in Rust?


I want to implement Writer-Reader pattern decoupled with a Channel. The problem is that Rust allows only 1 mutable reference at a time, but Writer must be able to write to the channel and Reader must be able to remove a value that it just read. Is there a conceptual violation of Rust assumptions?

Here is the code that expectedly can't be compiled:

// Channel
struct Channel {
    queue: Vec<String>
}

impl Channel {
    fn new() -> Self {
        Self {
            queue: vec!()
        }
    }

    fn write(&mut self, value: &str) {
        self.queue.push(value.to_string());
    }

    fn read(&mut self) -> Option<String> {
        if !self.queue.is_empty() {
            Some(self.queue.remove(self.queue.len() - 1))
        } else {
            None
        }
    }
}

// Writer

struct Writer<'a> {
    channel: &'a mut Channel
}

impl<'a> Writer<'a> {
    fn new(channel: &'a mut Channel) -> Self {
        Self {
            channel
        }
    }

    fn write(&mut self, value: &str) {
        self.channel.write(value);
    }
}

// Reader

struct Reader<'a> {
    channel: &'a mut Channel
}

impl<'a> Reader<'a> {
    fn new(channel: &'a mut Channel) -> Self {
        Self {
            channel
        }
    }

    fn read(&mut self) {
        if let Some(value) = self.channel.read() {
            print!("Read {}", value)
        }
    }
}

fn main() {
    let mut channel = Channel::new();
    let mut writer = Writer::new(&mut channel);
    let mut reader = Reader::new(&mut channel); // cannot borrow `channel` as mutable more than once at a time
    writer.write("test_value");
    reader.read();
}

Playground

What's the right way of doing that in Rust?

PS. It's mean to be executed in single-thread environment.


Solution

  • The static analysis performed by the borrow-checker follows the same rules as the Reader-Writer pattern: many readers of a resource are allowed at the same time, but as soon as (and as long as) a writer accesses this resource, no other reader or writer can access it. The borrow-checker enforces these rules at compile-time, thus it can prove these assumptions are correct only if the structure (in the sense of structural) of the program is made so.

    However, there are programs where we want to share a common resource between many places in the code, and it cannot be proven by a static analysis that these multiple usages won't conflict (with respect to the above rules). That's where interior-mutability come into play: the idea is to enforce these exact same rules at runtime (at the expense of a minimal cost). (the same exists in a parallel context with the usual read-write locks)

    Back to your example, you want to keep at the same time two references (reader and writer) to the same resource (channel) and use them freely to alter the common resource. Then, the references need fundamentally to be shared. As shown on this question, the & and &mut notations are often seen as const and mutable references (as a shorthand), but the real meaning should be though about in terms of shared or exclusive references. We must think in these shared/exclusive terms first, and the const/mutable aspect is only the consequence of this design choice.

    Although your example uses the terms read and write, it does not match the usual reader-writer pattern. Actually, these two operations are two different mutations of the underlying resource; they can be considered as two writers. A reader operation would be simply testing emptiness or getting the size.

    Please find below your example rewritten using RefCell in order to rely on interior-mutability. Note that, in these conditions, the references can actually be shared (&, not &mut) as required by your intended usage of reader and writer. The usages of reader and writer are intertwined so that the borrow-checker would find this conflictual if we used exclusive references.

    use std::cell::RefCell;
    
    struct Channel {
        queue: RefCell<Vec<String>>,
    }
    impl Channel {
        fn new() -> Self {
            Self {
                queue: RefCell::new(vec![]),
            }
        }
    
        fn write(
            &self,
            value: &str,
        ) {
            self.queue.borrow_mut().push(value.to_string());
        }
    
        fn read(&self) -> Option<String> {
            self.queue.borrow_mut().pop()
        }
    }
    
    struct Writer<'a> {
        channel: &'a Channel,
    }
    impl<'a> Writer<'a> {
        fn new(channel: &'a Channel) -> Self {
            Self { channel }
        }
    
        fn write(
            &self,
            value: &str,
        ) {
            self.channel.write(value);
        }
    }
    
    struct Reader<'a> {
        channel: &'a Channel,
    }
    impl<'a> Reader<'a> {
        fn new(channel: &'a Channel) -> Self {
            Self { channel }
        }
    
        fn read(&self) {
            if let Some(value) = self.channel.read() {
                println!("Read {}", value)
            }
        }
    }
    
    fn main() {
        let channel = Channel::new();
        let writer = Writer::new(&channel);
        let reader = Reader::new(&channel);
        writer.write("first value");
        reader.read();
        writer.write("second value");
        reader.read();
    }
    /*
    Read: first value
    Read: second value
    */