Concurrency 使用共享地图的惯用方法很好

Concurrency 使用共享地图的惯用方法很好,concurrency,go,Concurrency,Go,假设我有一个并发访问地图的程序,如下所示: 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

假设我有一个并发访问地图的程序,如下所示:

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()
})
这很糟糕,因为映射写入是非原子的。所以我可以使用读/写互斥锁

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()
})
这似乎很好,除了我们直接使用互斥体而不是通道


实现这一点的更为惯用的方法是什么?或者这是一个您真正需要的互斥锁的时刻吗?

我认为这取决于您的性能期望以及最终将如何使用此映射

当我研究同一个问题时,我遇到了一个非常有用的问题

我个人的反应是,除非你真的觉得需要使用互斥,否则你应该默认使用通道。惯用围棋的中心点是,如果您坚持使用更高级的通道功能,就不需要使用互斥体,也不用担心锁定问题。记住Go的座右铭:“通过交流来分享记忆,不要通过分享记忆来交流。”

还有一个花絮,在MarkSummerfield的文章中,我们详细介绍了构建安全地图以供并发使用的各种技术

要突出显示Go的创建者之一:

并发简化了同步
  • 不需要显式同步
  • 程序的结构是隐式同步的
当你使用互斥体这样的基本体时,由于你的程序比较复杂,这是非常非常非常难做到的。你已经被警告了

这里还有一段引用自:

在许多环境中,并发编程由于 实现对共享变量的正确访问所需的微妙之处。 Go鼓励采用不同的方法传递共享值 围绕在频道上,事实上,从来没有积极地由单独的用户共享 执行线程。只有一个goroutine可以访问处的值 任何时候都可以。这种方法可能走得太远了。参考计数 例如,最好在整数变量周围放置一个互斥体 例如。但作为一种高级方法,使用通道来控制 access使编写清晰、正确的程序变得更容易

  • 不能对消息队列使用锁本身。这就是频道的用途

  • 您可以通过通道模拟锁,但通道不是用来模拟锁的

  • 使用锁对共享资源进行并发安全访问

  • 使用通道进行并发安全消息队列


使用RWMutex来保护映射写入。

我想说,对于这个应用程序来说,互斥是很好的。用一种字体把它们包起来,这样你以后就可以像这样改变主意了。注意
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"))
}

这里有一种基于渠道的替代方法,使用渠道作为互斥机制:

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
})
func getKey(r*http.Request)字符串{…}
值\u ch:=make(chan-map[string]int,1)

这是一个非常恰当的答案和一个到文章的链接。我个人认为TS的方法对实现数据缓存很有用,在大多数其他情况下,重新设计应用程序以使用通道是完全可能的,这样可以消除很多头痛您永远不应该使用“默认通道”。:“使用最具表现力和/或最简单的方法。新手。过度使用频道。因为这是可能的。”所以“使用最具表现力的方法”-意思是先考虑一下。@Luke虽然我同意频道可能会被过度使用,但当你看看线程/锁定给程序员带来的主要困扰时,这就是为什么像Go这样的语言最初被创建的原因。实际上,线程并不难,难的是构建一个正确共享内存的程序,这就是为什么应该首先查看通道的原因。这意味着,如果渠道有效,就不要考虑其他选择。两者都应该平等对待。@Luke,相信我,Luke,我明白了。我把这篇文章交给OP的原因是为了让他们能够做出自己的判断。此外,我还就此事提供了我个人的回应,并将OP指向了一些额外的参考资料。你看,我一直在写同步锁定代码。要做到这一点并不容易,事实上你应该找个时间试试。请不要错误地引用我的话,显然互斥并不难使用。构建一个正确同步的程序是非常困难的,因此像Go这样的语言诞生了。但是,与RWMutex方法不同,在这里,即使没有编写者在场,读者也会相互阻止,不是吗?你能告诉我为什么他们会相互阻止吗?