9

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.

E_net4
  • 27,810
  • 13
  • 101
  • 139
l3utterfly
  • 2,106
  • 4
  • 32
  • 58
  • 5
    You can easily create a read-only wrapper type `Read` that wraps `Arc>` and keeps the inner `Arc>` private. `Read` would only expose the `read()` method of the inner `RwLock`, so the code that receives it couldn't modify the data. You'd send those wrappers to other threads, and keep the pristine `Arc>` in the main thread only. (You could even create a non-`Send` wrapper that holds `Arc>` in the main thread to avoid exposing them by accident - creation `Read` would be done in a method on the wrapper.) – user4815162342 Aug 24 '21 at 13:16
  • 2
    Here is [an example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=74ecbb2bbf071095dc68bb9050eed05f) of the wrapper. – user4815162342 Aug 24 '21 at 13:22
  • That is a great idea! This is probably what I'll end up doing if there's nothing in the `std` that does this already. – l3utterfly Aug 24 '21 at 13:27
  • 2
    Using an interface might help to cut out other behaviors, [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8b2bc266796b38debe2239d835f04fd3) – Ömer Erden Aug 24 '21 at 14:10
  • Another solution, simpler but with only runtime checking, is to have the main thread keep a read lock for the duration. This will deadlock if a worker thread attempts to take the write lock. – Jmb Aug 24 '21 at 15:08
  • @Jmb As I understood it, the main thread occasionally needs to acquire the write lock to update the data. Since this cannot be done without releasing the read lock first, there would be a window where no lock is held, so a worker could acquire the write lock itself. Also, the workers need to be able to outlive the main thread, so the lock could get released that way (unless the main thread intentionally leaked it, which sounds messy). – user4815162342 Aug 24 '21 at 16:18

1 Answers1

11

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());
}
Ibraheem Ahmed
  • 11,652
  • 2
  • 48
  • 54
  • For added safety, one could un-implement `Send` and `Sync` on `Lock` to prevent it from being sent or exposed to another thread by accident. – user4815162342 Aug 24 '21 at 13:33