nice, go-idiomatic way of using a shared map

2020-06-03 04:32发布

问题:

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?

回答1:

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.



回答2:

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



回答3:

  • 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.



回答4:

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.