英文:
nice, go-idiomatic way of using a shared map
问题
假设我有一个并发访问地图的程序,像这样:
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()
})
这段代码存在问题,因为地图的写操作不是原子的。所以我可以使用读写互斥锁(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()
})
这种方式看起来还不错,但问题在于我们直接使用了互斥锁而不是通道(channels)。
有没有更符合 Go 语言习惯的实现方式?或者说这种情况下互斥锁就是我们所需要的?
英文:
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
得分: 14
我认为这在很大程度上取决于你对性能的期望以及最终使用该地图的方式。
当我研究同样的问题时,我找到了这篇非常有帮助的文章,应该能回答你的问题。
我的个人回答是,除非你真的发现需要使用互斥锁,否则应该默认使用通道。Go的核心思想是,如果你坚持使用更高级的通道功能,就不需要使用互斥锁和担心锁定。记住Go的座右铭:“通过通信共享内存,而不是通过共享内存进行通信”。
还有一个小提示,在Mark Summerfield的Go书籍中有一个非常详细的关于构建安全地图以供并发使用的教程。
为了强调Go的创始人之一Rob Pike的幻灯片:
并发简化了同步
- 不需要显式同步
- 程序的结构隐式同步
当你选择使用像互斥锁这样的原语时,随着程序变得更加复杂,这变得非常非常难以正确实现。你已经被警告了。
此外,这是Golang官方网站上的一句引用:
在许多环境中,并发编程由于实现对共享变量的正确访问所需的微妙之处而变得困难。Go鼓励采用一种不同的方法,即通过通道传递共享值,并且实际上永远不会由单独的执行线程主动共享。在任何给定时间,只有一个goroutine可以访问该值。这种方法可能过于极端。例如,引用计数可能最好通过在整数变量周围放置互斥锁来完成。但作为一种高级方法,使用通道来控制访问使编写清晰、正确的程序更容易。
英文:
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
得分: 9
我认为互斥锁(mutexes)在这个应用程序中是可以的。可以将它们封装在一个类型中,这样你以后可以改变主意。请注意嵌入了sync.RWMutex
,这使得锁定更加简洁。
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"))
}
英文:
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"))
}
答案3
得分: 3
-
你不能直接使用锁来处理消息队列,这就是通道的作用。
-
你可以通过通道来模拟锁,但这不是通道的本意。
-
使用锁来实现对共享资源的并发安全访问。
-
使用通道来实现并发安全的消息排队。
使用RWMutex来保护对map的写操作。
英文:
-
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
得分: 3
这是一种基于通道的替代方法,使用通道作为互斥机制:
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
})
在这里,我们最初将资源放入一个共享通道中。然后,goroutine可以借用和归还该共享资源。然而,与使用RWMutex
的解决方案不同,多个读取者可能会相互阻塞。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论