Rust thread design issue


#1

Hi I hope someone can help with a design issue I am having trouble with.

I have a Container that contains a vector of traits. I like the container owning the MyTraits as the container can seralise and deseralise itself etc.

Say

pub trait MyTrait: Send + Sync {

    fn run(&mut self);

    fn set_property(&mut self);

    fn get_property(self);
}

struct Container {
    Vec<Box<MyTrait>>,
}

I need to be able to pass this container to a few threads. One web frontend thread that need to edit the mytrait properties in a webpage etc and another thread to manage ctrl-c as below.

crossbeam::scope(|scope| {

        let mut container = Container::new();

        mytrait creation etc ...
        container.add_mytrait(mytrait);

   
        scope.spawn(move || {
            web::serve(Arc::new(Mutex::new(container)));
        });

        scope.spawn(move || {
            container.run();
        });

        ctrlc::set_handler(move || {
            
            container.stop();
            std::process::exit(0);

        }).expect("Error setting Ctrl-C handler");
        
        container.wait_for_completion();
    });

I obviously can’t pass the container to more than one thread with an arc and a mutex. However, I don’t see how that would help as container.run() is a long running loop I can’t lock a mutex before run as no other threads would get a look in. I can’t clone the container as the MyTraits get cloned and I need their state to accessed from different threads. For example the run method my change the properties of the mytraits and that should be reflected in the web frontend.

Has anyone got advice about how to structure this ?

Thanks


#2

First of all, note that the forum software will render your code examples better if you tell it that it is Rust code, which can be done using the following syntax:

```rust
Put Rust code here
```

For short code snippets inside of regular text, like Mutex<T>, an alternative is the `Put code here` syntax, which has no syntax highlighting support.


Now, onto the problem at hand. Indeed, using a Mutex<Container> in order to lock the container before running container.run(), and unlock it after that would be problematic if container.run() is a long-running function, as it would stall any other thread which needs to access the container (including your Ctl+C handler, which you probably want to be snappy).

To improve upon this, a general approach would be to look for ways to lock smaller fractions of the container (so that other threads can concurrently work on other parts of the container), and to lock them for shorter periods of time (so that another thread which targets the same fraction of the container can access it more quickly).

The key to doing so is to identify transactions: elementary operations on the shared container which leave it in a consistent state that other threads can safely observe. In your case, if the trait objects inside of the container are independent from each other, MyTrait::set_property() and MyTrait::get_property() could be two such transactions, and so you might reach the conclusion that the appropriate granularity would be to lock trait objects individually by redesigning your shared container like this:

struct Container {
    Vec<Mutex<Box<MyTrait>>>,
}

Some important caveats of this finer-grained approach:

  • You need to define your transactions carefully. If you don’t, your threads will end up observing the shared container in an invalid state and doing strange things because of that. The finer-grained your transactions go, the harder this gets.
  • Even in the absence of contention, locking a mutex has an intrinsic cost (of the order of ~100ns last time I measured it on my Linux machines), so you don’t want to do it in a tight loop. If you know that some trait objects will always be accessed together, it’s better to group them under the same mutex. And if your vector of trait objects is very long, you may need to compromise by locking groups of trait objects instead of individual ones.
  • Finer-grained locking will not help that much when you have multiple threads that need to scan through the entire container, as they will end up fighting for access to the same individual trait objects anyhow. Here, I’m thinking about your container.run() and container.stop() methods, for example, which for all I know might well do something to every trait object inside of the container. There are many ways to cheat (for example iterating through the container in reverse orders in this specific case), but at some point you always end up hitting the intrinsic hardware limitation that shared mutable state simply does not scale well to lots of concurrent accesses.

#3

@HadrienG has already given an excellent answer to your question using mutexes, however you may also benefit from a really common idiom from the Go world:

Do not communicate by sharing memory; instead, share memory by communicating.

Instead of sharing the container across multiple threads and the overhead associated with that, you might consider a design where only one thread is able to access the resource and everyone else must send it messages via channels.

If you google that quote I’m sure you’ll find loads of articles explaining how the message passing way of thinking can be applied to applications. It’s proven itself to be very effective in the web world and as a way of decreasing complexity in parallel code.


#4

Just to clarify, both of the design approaches which @Michael-F-Bryan and I have discussed share the concern of identifying simple self-contained transactions on the shared state.

In the approach that I have discussed, you would then protect one or more of these transactions through locking (or another synchronization mechanism) in order to ensure that no other thread can interfere with them or observe them as half-done. Whereas in the approach that Michael has discussed, you would build a message-based communication protocol by which “external” threads can ask the “owner” thread to perform a set of transactions on the shared state, and freedom of interference would be ensured by virtue of the “owner” thread operating on the shared state in a sequential fashion, processing one message at a time.

Note that message passing has higher overhead than locking (or lower-level thread synchronization operations), and is thus best applied to relatively large operations on the shared state. On the other hand, it handles such coarse-grained operations better than locking, because other threads are not forced to block waiting for a reply, and can more easily go and do something else.


#5

In the end I grouped all the mutable fields of Container in a Arc<Mutex<BTreeMap<String, serde_json::Value>>>.
This allowed cloning the Container and I can now lock a smaller fraction of the container as HadrienG suggested.

Thanks