X-Git-Url: https://git.ralfj.de/rust-101.git/blobdiff_plain/18e6fec08956d95a3fd1b4b1ef2a7bb9620c5fcf..76de9227939c9244b1e5bd9364542c92991140c8:/workspace/src/part12.rs diff --git a/workspace/src/part12.rs b/workspace/src/part12.rs index 4996ac1..86fb4bb 100644 --- a/workspace/src/part12.rs +++ b/workspace/src/part12.rs @@ -1,128 +1,108 @@ -// Rust-101, Part 12: Concurrency, Arc, Send -// ========================================= - -use std::io::prelude::*; -use std::{io, fs, thread}; -use std::sync::mpsc::{sync_channel, SyncSender, Receiver}; -use std::sync::Arc; - - -// Before we come to the actual code, we define a data-structure `Options` to store all the information we need -// to complete the job: Which files to work on, which pattern to look for, and how to output.
-#[derive(Clone,Copy)] -pub enum OutputMode { - Print, - SortAndPrint, - Count, -} -use self::OutputMode::*; +// Rust-101, Part 12: Rc, Interior Mutability, Cell, RefCell +// ========================================================= -pub struct Options { - pub files: Vec, - pub pattern: String, - pub output_mode: OutputMode, -} +use std::rc::Rc; +use std::cell::{Cell, RefCell}; -// The first function reads the files, and sends every line over the `out_channel`. -fn read_files(options: Arc, out_channel: SyncSender) { - for file in options.files.iter() { - // First, we open the file, ignoring any errors. - let file = fs::File::open(file).unwrap(); - // Then we obtain a `BufReader` for it, which provides the `lines` function. - let file = io::BufReader::new(file); - for line in file.lines() { - let line = line.unwrap(); - // Now we send the line over the channel, ignoring the possibility of `send` failing. - out_channel.send(line).unwrap(); - } - } - // When we drop the `out_channel`, it will be closed, which the other end can notice. + +#[derive(Clone)] +struct Callbacks { + callbacks: Vec>, } -// The second function filters the lines it receives through `in_channel` with the pattern, and sends -// matches via `out_channel`. -fn filter_lines(options: Arc, - in_channel: Receiver, - out_channel: SyncSender) { - // We can simply iterate over the channel, which will stop when the channel is closed. - for line in in_channel.iter() { - // `contains` works on lots of types of patterns, but in particular, we can use it to test whether - // one string is contained in another. This is another example of Rust using traits as substitute for overloading. - if line.contains(&options.pattern) { - unimplemented!() - } +impl Callbacks { + pub fn new() -> Self { + Callbacks { callbacks: Vec::new() } } -} -// The third function performs the output operations, receiving the relevant lines on its `in_channel`. -fn output_lines(options: Arc, in_channel: Receiver) { - match options.output_mode { - Print => { - // Here, we just print every line we see. - for line in in_channel.iter() { - unimplemented!() - } - }, - Count => { - // We are supposed to count the number of matching lines. There's a convenient iterator adapter that - // we can use for this job. - unimplemented!() - }, - SortAndPrint => { - // We are asked to sort the matching lines before printing. So let's collect them all in a local vector... - let mut data: Vec = in_channel.iter().collect(); - // ...and implement the actual sorting later. + // Registration works just like last time, except that we are creating an `Rc` now. + pub fn register(&mut self, callback: F) { + unimplemented!() + } + + pub fn call(&self, val: i32) { + // We only need a shared iterator here. Since `Rc` is a smart pointer, we can directly call the callback. + for callback in self.callbacks.iter() { unimplemented!() } } } -// With the operations of the three threads defined, we can now implement a function that performs grepping according -// to some given options. -pub fn run(options: Options) { - // We move the `options` into an `Arc`, as that's what the thread workers expect. - let options = Arc::new(options); - - // This sets up the channels. We use a `sync_channel` with buffer-size of 16 to avoid needlessly filling RAM. - let (line_sender, line_receiver) = sync_channel(16); - let (filtered_sender, filtered_receiver) = sync_channel(16); - - // Spawn the read thread: `thread::spawn` takes a closure that is run in a new thread. - let options1 = options.clone(); - let handle1 = thread::spawn(move || read_files(options1, line_sender)); - - // Same with the filter thread. - let options2 = options.clone(); - let handle2 = thread::spawn(move || { - filter_lines(options2, line_receiver, filtered_sender) - }); - - // And the output thread. - let options3 = options.clone(); - let handle3 = thread::spawn(move || output_lines(options3, filtered_receiver)); - - // Finally, wait until all three threads did their job. - handle1.join().unwrap(); - handle2.join().unwrap(); - handle3.join().unwrap(); +// Time for a demo! +fn demo(c: &mut Callbacks) { + c.register(|val| println!("Callback 1: {}", val)); + c.call(0); c.clone().call(1); } -// Now we have all the pieces together for testing our rgrep with some hard-coded options. pub fn main() { - let options = Options { - files: vec!["src/part10.rs".to_string(), - "src/part11.rs".to_string(), - "src/part12.rs".to_string()], - pattern: "let".to_string(), - output_mode: Print - }; - run(options); + let mut c = Callbacks::new(); + demo(&mut c); +} + +// ## Interior Mutability + +// So, let us put our counter in a `Cell`, and replicate the example from the previous part. +fn demo_cell(c: &mut Callbacks) { + { + let count = Cell::new(0); + // Again, we have to move ownership if the `count` into the environment closure. + c.register(move |val| { + // In here, all we have is a shared borrow of our environment. But that's good enough for the `get` and `set` of the cell! + let new_count = count.get()+1; + count.set(new_count); + println!("Callback 2: {} ({}. time)", val, new_count); + } ); + } + + c.call(2); c.clone().call(3); } -// **Exercise 12.1**: Change rgrep such that it prints not only the matching lines, but also the name of the file -// and the number of the line in the file. You will have to change the type of the channels from `String` to something -// that records this extra information. +// ## `RefCell` + +// Our final version of `Callbacks` puts the closure environment into a `RefCell`. +#[derive(Clone)] +struct CallbacksMut { + callbacks: Vec>>, +} + +impl CallbacksMut { + pub fn new() -> Self { + CallbacksMut { callbacks: Vec::new() } + } + + pub fn register(&mut self, callback: F) { + unimplemented!() + } + + pub fn call(&mut self, val: i32) { + for callback in self.callbacks.iter() { + // We have to *explicitly* borrow the contents of a `RefCell` by calling `borrow` or `borrow_mut`. + let mut closure = callback.borrow_mut(); + // Unfortunately, Rust's auto-dereference of pointers is not clever enough here. We thus have to explicitly + // dereference the smart pointer and obtain a mutable borrow of the content. + (&mut *closure)(val); + } + } +} + +// Now we can repeat the demo from the previous part - but this time, our `CallbacksMut` type +// can be cloned. +fn demo_mut(c: &mut CallbacksMut) { + c.register(|val| println!("Callback 1: {}", val)); + c.call(0); + + { + let mut count: usize = 0; + c.register(move |val| { + count = count+1; + println!("Callback 2: {} ({}. time)", val, count); + } ); + } + c.call(1); c.clone().call(2); +} +// **Exercise 12.1**: Write some piece of code using only the available, public interface of `CallbacksMut` such that a reentrant call to a closure +// is happening, and the program aborts because the `RefCell` refuses to hand out a second mutable borrow of the closure's environment.