l3utterfly
l3utterfly

Reputation: 2184

How do I send read-only data to other threads without copying?

I'm trying to send a "view" of a read-only data to another thread for processing. Basically the main thread does work, and continuously updates a set of data. Whenever an update occurs, the main thread should send the updated data down to other threads where they will process it in a read-only manner. I do not want to copy the data as it may be very large. (The main thread also keeps a "cache" of the data in-memory anyway.)

I can achieve this with Arc<RwLock<T>>, where T being my data structure.

However, there is nothing stopping the side threads updating the data. The side threads can simply call lock() and the write to the data.

My question is there something similar to RwLock where the owner/creator of it has the only write access, but all other instances have read-only access? This way I will have compile time checking of any logic bugs that may occur via side threads accidentally updating data.

Regarding these questions:

The above questions suggest solving it with Arc<Mutex<T>> or Arc<RwLock<T>> which is all fine. But it still doesn't give compile time enforcement of only one writer.

Additionally: crossbeam or rayon's scoped threads don't help here as I want my side threads to outlive my main thread.

Upvotes: 10

Views: 4159

Answers (1)

Ibraheem Ahmed
Ibraheem Ahmed

Reputation: 13588

You can create a wrapper type over an Arc<RwLock<T>> that only exposes cloning via a read only wrapper:

mod shared {
    use std::sync::{Arc, LockResult, RwLock, RwLockReadGuard, RwLockWriteGuard};

    pub struct Lock<T> {
        inner: Arc<RwLock<T>>,
    }

    impl<T> Lock<T> {
        pub fn new(val: T) -> Self {
            Self {
                inner: Arc::new(RwLock::new(val)),
            }
        }

        pub fn write(&self) -> LockResult<RwLockWriteGuard<'_, T>> {
            self.inner.write()
        }

        pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
            self.inner.read()
        }

        pub fn read_only(&self) -> ReadOnly<T> {
            ReadOnly {
                inner: self.inner.clone(),
            }
        }
    }

    pub struct ReadOnly<T> {
        inner: Arc<RwLock<T>>,
    }

    impl<T> ReadOnly<T> {
        pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
            self.inner.read()
        }
    }
}

Now you can pass read only versions of the value to spawned threads, and continue writing in the main thread:

fn main() {
    let val = shared::Lock::new(String::new());

    for _ in 0..10 {
        let view = val.read_only();
        std::thread::spawn(move || {
            // view.write().unwrap().push_str("...");
            // ERROR: no method named `write` found for struct `ReadOnly` in the current scope
            println!("{}", view.read().unwrap());
        });
    }

    val.write().unwrap().push_str("...");
    println!("{}", val.read().unwrap());
}

Upvotes: 12

Related Questions