Reputation: 3318
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
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)
}
Upvotes: 9