Say I had a program with concurrent access to a map, like this:
func getKey(r *http.Request) string { ... }
values := make(map[string]int)
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
fmt.Fprint(w, values[key])
})
http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
values[key] = rand.Int()
})
This is bad since map writes are non-atomic. So I could use a read/write mutex
func getKey(r *http.Request) string { ... }
values := make(map[string]int)
var lock sync.RWMutex
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
lock.RLock()
fmt.Fprint(w, values[key])
lock.RUnlock()
})
http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
lock.Lock()
values[key] = rand.Int()
lock.Unlock()
})
Which seems fine except for the fact that we're directly using mutexes and not channels.
What's a more go-idiomatic way of implementing this? Or is this one of those times where a mutex is really all you need?
I think this largley depends on your performance expectations and how this map will ultimately be used.
When I was researching this same question I came across this very helpful article that should answer your question.
My personal response is that you should use channels by default unless you really find the need to use a mutex. Kind of the central point of idiomatic Go is that you don't need to use mutexes and worry about locking if you stick with the more high-level channels functionality. Remember Go's motto: "Share memory by communicating, don't communicate by sharing memory."
One more tidbit, there is a very detailed tour of different techniques of building a safe map for concurrent usage in Mark Summerfield's Go book.
To highlight Rob Pike's slide, one of the creator's of Go:
Concurrency Simplifies Synchronization
- No explicit synchronization is needed
- The structure of the program is implicitly synchronized
When you go down the path of using a primitive like a mutex, as your program is more complicated this is extremely, extremely hard to get right. You have been warned.
Also here is a quote from the Golang site itself:
Concurrent programming in many environments is made difficult by the
subtleties required to implement correct access to shared variables.
Go encourages a different approach in which shared values are passed
around on channels and, in fact, never actively shared by separate
threads of execution. Only one goroutine has access to the value at
any given time. This approach can be taken too far. Reference counts
may be best done by putting a mutex around an integer variable, for
instance. But as a high-level approach, using channels to control
access makes it easier to write clear, correct programs.
I would say mutexes are fine for this application. Wrap them in a type so you can change your mind later like this. Note the embedding of sync.RWMutex
then which makes the locking neater.
type thing struct {
sync.RWMutex
values map[string]int
}
func newThing() *thing {
return &thing{
values: make(map[string]int),
}
}
func (t *thing) Get(key string) int {
t.RLock()
defer t.RUnlock()
return t.values[key]
}
func (t *thing) Put(key string, value int) {
t.Lock()
defer t.Unlock()
t.values[key] = value
}
func main() {
t := newThing()
t.Put("hello", 1)
t.Put("sausage", 2)
fmt.Println(t.Get("hello"))
fmt.Println(t.Get("potato"))
}
Playground link
You cannot use locks per se for message queues. That's what channels are for.
You can simulate locks by channels, but that's not what channels are for.
Use locks for concurrent safe access to shared resources.
Use channels for concurrent safe message queuing.
Use RWMutex to protect map writes.
Here's an alternative channel-based approach, using the channel as a mechanism for mutual exclusion:
func getKey(r *http.Request) string { ... }
values_ch := make(chan map[string]int, 1)
values_ch <- make(map[string]int)
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
values := <- values_ch
fmt.Fprint(w, values[key])
values_ch <- values
})
http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
key := getKey(r)
values := <- values_ch
values[key] = rand.Int()
values_ch <- values
})
where we initially put the resource in a shared channel. Then the goroutines can borrow and return that shared resource. However, unlike the solution with RWMutex
, multiple readers can block each other.