Sharing Data with Read/Write Locks

It's a really common pattern to have some data that changes infrequently, mostly accessed by worker threads---but occasionally, you need to change it.

The code for this is in rwlock in the code/02_threads directory.

We're going to use once_cell in this example, so add it with cargo add once_cell. once_cell is on its way into the standard library.

Let's build a simple example of this in action:

use std::sync::RwLock;
use once_cell::sync::Lazy;

static USERS: Lazy<RwLock<Vec<String>>> = Lazy::new(|| RwLock::new(build_users()));

fn build_users() -> Vec<String> {
    vec!["Alice".to_string(), "Bob".to_string()]
}

// Borrowed from last week!
pub fn read_line() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    input.trim().to_string()
}

fn main() {
    std::thread::spawn(|| {
        loop {
            println!("Current users (in a thread)");
            let users = USERS.read().unwrap();
            println!("{users:?}");
            std::thread::sleep(std::time::Duration::from_secs(3));
        }
    });

    loop {
        println!("Enter a name to add to the list (or 'q' to quit):");
        let input = read_line();
        if input == "q" {
            break;
        }
        let mut users = USERS.write().unwrap();
        users.push(input);
    }
}

Notice that we've used the Lazy pattern we talked about last week: the static variable is only initialized when someone looks at it.

We've wrapped the list of names in an RwLock. This is like a Mutex, but you can either read or write to it. You can have multiple readers, but only one writer.

Uncontested read is very fast. Pausing for a write is very slightly slower than a Mutex, but not by much.