FWIW, Dashmap
is a great crate for scaling up concurrent accesses, since, indeed, the inner locks it uses are more fine grained, since their HashMap
is actually sharded. For instance, taking imaginary parameters, if you had 1000 keys, DashMap
may split that map into four 250
-key-long sub-HashMap
s (a "shard"), which are the things being Mutex
ed RwLock
-ed.
This way, when one of the shards is being mutated (moment where exclusive access is required), then at least the other shards can live their life.
The reason I am explaining this, is that given your use case, which may not involve too many keys, @jhiver, I'm afraid DashMap
may not operate that much better than a regular RwLock<HashMap<...>>
(obviously these things should be benchmarked, it;'s very hard to correctly speculate about the performance of parallel accesses).
Given your use-case, @jhiver, it looks like you won't have that many keys, or at least, won't be "adding" keys to the map that often.
If that's the case, then a good trick can be to use more fine-grained locks: by having them nested one level deeper:
lazy_static! {
static ref DICT: HashMap<String, RwLock<DestLMI>> = {
let mut map = HashMap::new();
map.insert("euro".into(), RwLock::new(...));
map.insert("dollar".into(), RwLock::new(...));
map
};
}
This way, when locking, you only lock one single key (currency) each time, so that two threads operating on different keys / currencies won't step on each other's toes.
Now, you may have notice that in order for that to work, you need to know the different currencies in advance, since once the static
is created, that part is fixed (at which point you could just go and use a struct
rather than a map ).
If that is not possible for your use-case (you need to be able, at runtime, to add or remove keys / currencies), then the solution is to add another layer of locking, precisely for that purpose. This way you differentiate from locking for updating a single currency (efficient), vs. locking to add / remove currencies altogether (which requires locking the whole map, thus inefficient, but at least that should not happen very often for it to matter):
lazy_static! {
static ref DICT: RwLock<
HashMap<String, RwLock<DestLMI>>
> = RwLock::new({
let mut map = HashMap::new();
map.insert("euro".into(), RwLock::new(...));
map.insert("dollar".into(), RwLock::new(...));
map
});
}
This way, to add a key, you DICT.write().unwrap()
(lock the whole map), and then get &mut
(exclusive) access to the map to do, with it, as you see fit), and otherwise DICT.read().unwrap().get("euro")
to, through a shared access to the whole map, access a single field where you may get shared (.read().unwrap()
) or exclusive (.write().unwrap()
) access to that currency's DestLMI
, depending on your needs.
If you feel like it, you can then get rid of those .unwrap()
s by using ::parking_lot
's locks, and you can still use a DashMap
as an optimization over the outer RwLock
.
Which gives:
Final design
use ::dashmap::DashMap as ConcurrentMap;
use ::lazy_static::lazy_static;
use ::parking_lot::RwLock;
lazy_static! {
static ref DICT: ConcurrentMap<String, RwLock<DestLMI>> = {
...
};
};
So that:
-
to update the keys, you simply DICT.insert(...)
/ DICT.remove(...)
(that will lock a whole shard when doing it, but there is not better way around it);
-
to read an entry you DICT.get("euro").read()
, and to mutate one you DICT.get("euro").write()
. This only locks a single currency, leaving all the other currencies uncontested.
- (instead of
DICT.get_mut("euro")
, since that would cause a whole shard to be locked for the duration of the mutation).