Lazy_static concurrent access

Hello Rust-Users!

I'm using lazy_static to create a singleton like so:

lazy_static! {
    static ref DICT : Mutex<HashMap<String, DestLMI>> = 
Mutex::new(HashMap::new());
}

Trouble is, having to use lazy_static means i need to lock the entire hashmap each and everytime I need to do something with it. It would be much nicer to be able to have to lock only the relevant key(s) which I'm using.

Any workarounds? My idea is to build a service that self-updates in the background. For example, a hash-map of forex, where each currency would self-update independently. Having to lock the entire thing to either read or update a single key sort of blows...

You might be wondering why I'm trying to use a static value instead of having it in main and passing it around by reference... Ultimately I want to build a self updating library that can be called from C code, where you first start the service, then it self updates the data in the background.

Thanks in advance for your suggestions & help!

Kind Regards
JM

You can use a DashMap instead.

lazy_static! {
    static ref DICT: DashMap<String, DestLMI> = DashMap::new();
}
2 Likes

Woah that's a quick reply... I'm going to take a close look at this. Thanks a million, I'll let you know how it went!

1 Like

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-HashMaps (a "shard"), which are the things being Mutexed 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 :thinking:).

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).
4 Likes

Woah, so much information in that post... I didn't need something that sophisticated for my use case (happy to just replace the values instead of mutating them) so a simple DashMap seems to have done it. Kudos though as I've learned a lot from reading this - thanks!

Hey @alice, thanks a lot, DashMap worked like a charm.

Declaration:

lazy_static! {
    static ref DICT: DashMap<String, DestLMI> = DashMap::new();
}

Inserting:

DICT.insert(k, v);

Reading:

match DICT.get(k) {
    Some(value) => do_something(&value),
    None => None
}

Easy enough... thanks a lot to all!

2 Likes