Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use RWMutex and upgrade locks

Tags:

go

I'm trying to implement the pattern of access to a map that allows multiple readers and only one writer and where the key is only written to the map once.

I want to make sure that the key, if it exists, is only ever placed into the map one time. And then once the key is added, the readers obtain the value from the map.

It think this is correct, although the main thread could exit before all the goroutines finish, hence I don't always see 10 Exit's printed. Is this the correct pattern of implementation?

package main

import (
    "fmt"
    "sync"
    "time"
)

var downloads map[string]*sync.WaitGroup
var downloadsLock sync.RWMutex

func concurrentAccessTest() {
    // Acquire the read lock to check if the items is already there.
    downloadsLock.RLock()
    if wg, ok := downloads["item"]; ok {
        // Item exists
        downloadsLock.RUnlock()
        wg.Wait()
    } else {
        downloadsLock.RUnlock()

        fmt.Println("Upgrade Lock")
        // Item does not exist we need to add it to the map
        downloadsLock.Lock()
        fmt.Println("Writer lock obtained")
        // Check another thread hasn't gone down this path and added the item to the map
        if wg, ok := downloads["item"]; ok {
            // Item exists, no need to add it, unlock and wait
            downloadsLock.Unlock()
            wg.Wait()
            fmt.Println("Ok, now we can proceed")
        } else {
            // We are first in. Add and unlock
            wg = &sync.WaitGroup{}
            downloads["item"] = wg
            downloadsLock.Unlock()

            wg.Add(1)

            fmt.Println("Do some time consuming stuff!")
            time.Sleep(5 * time.Second)
            wg.Done()
        }
    }

    fmt.Println("Exit")
}

func main() {
    downloads = make(map[string]*sync.WaitGroup)

    // Add to the map
    for i := 0; i < 10; i++ {
        go concurrentAccessTest()
    }

    concurrentAccessTest()

    // Wait for all threads to exit
    fmt.Println("Done!")
}
like image 244
hookenz Avatar asked Oct 29 '22 13:10

hookenz


1 Answers

As your code shows, there's no way to upgrade from a read to a write lock, you have to release the read lock and then take the write lock.

I think you're close, the use of RWMutex itself looks fine, however i believe you have a potential issue with calling wg.Add(1) after releasing the lock. There's a chance that one of the other goroutines will read the WaitGroup out of the map and call Wait() before the call to Add() is executed, which according to the WaitGroup docs is an issue. The docs say Note that calls with a positive delta that occur when the counter is zero must happen before a Wait. You can easily fix this by moving the wg.Add(1) call before the Unlock()

like image 153
superfell Avatar answered Nov 01 '22 00:11

superfell