12

假设我有一个程序可以同时访问地图,如下所示:

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()
})

除了我们直接使用互斥锁而不是通道这一事实之外,这似乎很好。

什么是更惯用的实现方式?或者这是您真正需要互斥锁的时代之一?

4

4 回答 4

13

我认为这很大程度上取决于您对性能的期望以及这张地图最终将如何使用。

当我研究同样的问题时,我遇到了这篇非常有用的文章,应该可以回答你的问题

我个人的回答是,除非您真的发现需要使用互斥锁,否则您应该默认使用通道。惯用 Go 的中心点是,如果您坚持使用更高级的通道功能,则无需使用互斥锁并担心锁定。记住 Go 的座右铭:“通过通信共享内存,不要通过共享内存进行通信。”

还有一个花絮,在 Mark Summerfield 的Go book中有一个非常详细的关于构建安全地图以供并发使用的不同技术的教程。

为了突出Rob Pike 的幻灯片,Go 的创建者之一:

并发简化了同步

  • 不需要显式同步
  • 程序的结构是隐式同步的

当您走上使用像互斥锁这样的原语的道路时,由于您的程序更加复杂因此很难做到正确。你被警告了。

这里也引用了Golang 网站本身的一段话:

由于实现对共享变量的正确访问所需的微妙之处,许多环境中的并发编程变得困难。Go 鼓励一种不同的方法,在这种方法中,共享值在通道上传递,实际上,从不被单独的执行线程主动共享。在任何给定时间,只有一个 goroutine 可以访问该值。这种方法可能太过分了。例如,引用计数最好通过在整数变量周围放置互斥锁来完成。但是作为一种高级方法,使用通道来控制访问可以更容易地编写清晰、正确的程序。

于 2013-08-12T16:43:46.490 回答
9

我会说互斥锁非常适合这个应用程序。将它们包装在一个类型中,以便您以后可以像这样改变主意。注意 then 的嵌入,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"))
}

游乐场链接

于 2013-08-12T17:35:02.187 回答
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,多个阅读器可以相互阻止。

于 2013-08-13T02:25:18.820 回答
3
  • 您不能将锁本身用于消息队列。这就是渠道的用途。

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

  • 使用锁实现对共享资源的并发安全访问。

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

使用 RWMutex 保护映射写入。

于 2013-08-12T17:14:07.243 回答