Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Locking golang recursive map

I have a recursive map-like structure which looks like this:

type RecurseTable struct {
    Table map[string]*RecurseTable
    // Other fields
    sync.RWMutex
}

If I am going to access this structure from multiple goroutines, how exactly do I go about locking it? Let's say I'm reading from the top-level map and writing to the third-level, nested map. Is it accurate to say that this wouldn't cause any problems because changing the third level (and hence indirecting through two pointers) shouldn't affect the top level map?

Similarly, if I have a pool of goroutines all modifying information in the second-level, nested struct, would it be the case I only need to lock each second-level map because the top-level map only contains a pointer to the nested RecurseTable? Or is it the case that I have to lock both the top-level map as well as the nested struct, because the struct could somehow be reallocated in memory, causing a change to the pointer stored as a value in the top-level map?

Another situation would be adding keys to the top-level map whilst reading from the second-level struct. Is it safe to assume that any reorganising of the top-level map due to the new key wouldn't affect the location of the second-level struct in memory, and hence it wouldn't be necessary to lock the struct whilst reading?

My goal here is to minimise global locks of the entire recursive structure so that my goroutines can work on different parts of the structure in parallel, with minimal lock contention. I guess the core of my question is about how maps in Golang resize.

like image 711
a3onstorm Avatar asked Sep 30 '22 03:09

a3onstorm


2 Answers

From the Go maps in action blogpost :

Maps are not safe for concurrent use: it's not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex.

As you are feeding the map pointers to other maps, you can safely lock a map independently from the others (siblings or descendants): even if you delete a map entry while using its descendants, it won't be a problem since their references will be kept until you release the pointers (ie the var holding them is deleted).

like image 172
Elwinar Avatar answered Dec 04 '22 14:12

Elwinar


One approach would be to not export the Table member and provide Get/Set methods that lock appropriately. For example:

type RecurseTable struct {
    table map[string]*RecurseTable
    // Other fields
    sync.RWMutex
}

func New() *RecurseTable {
    return &RecurseTable{
        table: make(map[string]*RecurseTable),
    }
}

func (r *RecurseTable) Get(key string) (*RecurseTable, bool) {
    r.RLock()
    defer r.RUnlock()
    return r.table[key]
}

func (r *RecurseTable) Set(key string, value *RecurseTable) {
    r.Lock()
    defer r.Unlock()
    r.table[key] = value
}  

This way the only way to access the map's data is via the methods which guard access to the map. Each value is then guarded independently.

like image 27
Ian Davis Avatar answered Dec 04 '22 16:12

Ian Davis