Shared Mutex/mutable and Arc/immutable

I found myself in a situation where I have objects that I need to modify in different tasks, but I also want to store these objects in a HashSet. To solve the first part I have:

struct Peer {
  // ...
}

struct SharedState {
  // ...
  id_peers: HashMap<i64, Arc<Mutex<Peer>>>,
  hash_peers: HashMap<String, Arc<Mutex<Peer>>>,
  // ...
}

But when I want to stick Peer entries in a HashSet (with a custom hasher) I run into problems, because I don't want a custom hasher that needs to lock() a mutex for each entry -- in particular because this is a tokio::sync::Mutex, and I'm not really sure how an async custom hasher would look. Anywho ..

Conceptually, I want to do something like this:

struct SharedState {
  // ...
  id_peers: HashMap<i64, Arc<Mutex<Arc<Peer>>>>,
  // ...
  connected: HashSet<Arc<Peer>>,
  // ...
}

The Peers will never need write access when accessed through the connected.

Is there some [better] way to do this?

You can't put mutable values in a HashSet because the hash of a value must never change. That's why types with interior mutability (such as Mutex) cannot implement Hash.

Why do you need a hash set of peers and why do you need shared mutable access to peers? Maybe there is another approach to your problem.

1 Like

Generally it is recommended to not put IO resources behind mutexes. I can't tell what you put inside Peer, but that's what I'm guessing is there.

1 Like

If you never have two different Peers that are equal (but you might have two different Arcs pointing to the same Peer), then you could use the by_address crate or similar to store them in a HashSet using their memory addresses as the keys.

The type would be HashSet<ByAddress<Arc<Mutex<Peer>>>>.

2 Likes

Or, if you can separate the parts of a Peer that need exclusive/mutable access from the parts that are used for hashing and comparison, you can have one or more Mutexes inside the Peer, wrapping only the fields that need it. The Hash impl would read only from the other fields, without locking the Mutex(es). Then you can store Arc<Peer> in your maps and sets.

1 Like

Yes, this was why I noted that I will never need write access to the Peers through the connected member (the HashSet). I.e. the HashSet connected is only used to read peers.

I need shared mutable access to peers because they all have their own event queue, and each peer may need to add events to other peers. The reason I want to use a HashSet is because Peers can be connected or unconnected (unconnected Peers still exists), and I want to be able to look up if a Peer is connected or not without having to lock() them (which is how I'm currently doing it).

There are many ways to do this; I could simply store the database id of each peer in the HashSets -- however this is one of those learning projects that I'm using to do things I don't really already know how to do. I'm essentially trying to experiment with moving the state information out of the object and let the containers holding the objects denote their state, and - in particular - I'm looking for ways to allow the object itself (the Peer) to be the key. (There's a longer story behind this relating to a C++ project that I'm planning on oxidizing).

Don't worry, the sockets are kept strictly within their respective tokio tasks. The Peer structures just contain socket addresses, certificates, some state information, queues and a tokio::sync::Notify.

Hey, now that's really neat! And yes, each Peer can be uniquely identified by their address.

I'd like to clarify that even if you only have read-only access to peers through the HashSet, it is still invalid to mutate it through another handle. If you mutate a value so that its hash changes, the hash set will become invalid. And if you don't mutate it, you can just use Arc without Mutex (like @mbrubeck suggested).

You mentioned that you use tokio. Perhaps you can use tokio's queues for communication? Their senders and receivers are owned, so you don't need any kind of shared access to use them (shared access is handled internally). Thanks to these queues (and similar queues in std or crossbeam for synchronous code), it's rarely needed to use Arc<Mutex<_>> at all.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.