Raggaer
Raggaer

Reputation: 3318

Go goroutine lock and unlock

I been reading about goroutines and the sync package and my question is... Do I always need to lock unlock when reading writting to data on different goroutines?

For example I have a variable on my server

config := make(map[string]string)

Then on different goroutines I want to read from config. Is it safe to read without using sync or it is not?

I guess writting needs to be done using the sync package. but I am not sure about reading

For example I have a simple in-memory cache system

type Cache interface {
    Get(key string) interface{}
    Put(key string, expires int64, value interface{})
}

// MemoryCache represents a memory type of cache
type MemoryCache struct {
    c map[string]*MemoryCacheValue
    rw sync.RWMutex
}

// MemoryCacheValue represents a memory cache value
type MemoryCacheValue struct {
    value interface{}
    expires int64
}

// NewMemoryCache creates a new memory cache
func NewMemoryCache() Cache {
    return &MemoryCache{
        c: make(map[string]*MemoryCacheValue),
    }
}

// Get stores something into the cache
func (m *MemoryCache) Get(key string) interface{} {
    if v, ok := m.c[key]; ok {
        return v
    }
    return nil
}

// Put retrieves something from the cache
func (m *MemoryCache) Put(key string, expires int64, value interface{}) {
    m.rw.Lock()
    m.c[key] = &MemoryCacheValue{
        value,
        time.Now().Unix() + expires,
    }
    m.rw.Unlock()
}

I am acting safe here or I still need to lock unlock when I want to only read?

Upvotes: 1

Views: 6473

Answers (1)

Kaedys
Kaedys

Reputation: 10128

You're diving into the world of race conditions. The basic rule of thumb is that if ANY routine writes to or changes a piece of data that can be or is read by (or also written to) by any number of other coroutines/threads, you need to have some sort of synchronization system in place.

For example, lets say you have that map. It has ["Joe"] = "Smith" and ["Sean"] = "Howard" in it. One goroutine wants to read the value of ["Joe"]. Another routine is updating ["Joe"] to "Cooper". Which value does the first goroutine read? Depends on which goroutine gets to the data first. That's the race condition, the behavior is undefined and unpredictable.

The easiest method to control that access is with a sync.Mutex. In your case, since some routines only need to read and not write, you can instead use a sync.RWMutex (main difference is that a RWMutex allows any number of threads to read, as long as none are trying to write). You would bake this into the map using a structure like this:

type MutexMap struct {
    m map[string]string
    *sync.RWMutex
}

Then, in routines that need to read from the map, you would do:

func ReadSomething(o MutexMap, key string) string {
    o.RLock() // lock for reading, blocks until the Mutex is ready
    defer o.RUnlock() // make SURE you do this, else it will be locked permanently
    return o.m[key]
}

And to write:

func WriteSomething(o MutexMap, key, value string) {
    o.Lock() // lock for writing, blocks until the Mutex is ready
    defer o.Unlock() // again, make SURE you do this, else it will be locked permanently
    o.m[key] = value
}

Note that both of these could be written as methods of the struct, rather than functions, if desired.


You can also approach this using channels. You make a controller structure that runs in a goroutine, and you make requests to it over channels. Example:

package main

import "fmt"

type MapCtrl struct {
    m       map[string]string
    ReadCh  chan chan map[string]string
    WriteCh chan map[string]string
    QuitCh  chan struct{}
}

func NewMapController() *MapCtrl {
    return &MapCtrl{
        m:       make(map[string]string),
        ReadCh:  make(chan chan map[string]string),
        WriteCh: make(chan map[string]string),
        QuitCh:  make(chan struct{}),
    }
}

func (ctrl *MapCtrl) Control() {
    for {
        select {
        case r := <-ctrl.ReadCh:
            fmt.Println("Read request received")
            retmap := make(map[string]string)
            for k, v := range ctrl.m { // copy map, so it doesn't change in place after return
                retmap[k] = v
            }
            r <- retmap
        case w := <-ctrl.WriteCh:
            fmt.Println("Write request received with", w)
            for k, v := range w {
                ctrl.m[k] = v
            }
        case <-ctrl.QuitCh:
            fmt.Println("Quit request received")
            return
        }
    }
}

func main() {
    ctrl := NewMapController()
    defer close(ctrl.QuitCh)
    go ctrl.Control()

    m := make(map[string]string)
    m["Joe"] = "Smith"
    m["Sean"] = "Howard"
    ctrl.WriteCh <- m

    r := make(chan map[string]string, 1)
    ctrl.ReadCh <- r
    fmt.Println(<-r)
}

Runnable version

Upvotes: 9

Related Questions