使用共享映射的不错、go语言惯用方式

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

这很糟糕,因为map的写操作是非原子的。所以我可以使用读写互斥锁。

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语言的实现方式?或者这是那种只需要互斥锁就足够的时候?

4个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
14

我认为这主要取决于您的性能期望以及最终使用此地图的方式。

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

我的个人建议是除非您真的发现需要使用互斥锁,否则默认情况下应该使用通道。 Go 的典型特点就是如果您坚持使用更高级别的通道功能,则无需使用互斥锁并担心锁定。 记住 Go 的座右铭:“通过通信共享内存,而不是通过共享内存通信。”

还有一个小贴士,Mark Summerfield 的Go 书籍中详细介绍了构建安全地图以供并发使用的不同技术。

重点在Rob Pike 的幻灯片上,他是 Go 的创造者之一:

并发简化了同步

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

当你使用像互斥锁这样的原始工具时,随着程序变得越来越复杂,这非常非常难以实现。你已经被警告了。

此外,这里有Golang官网本身上的一句话:

在许多环境下进行并发编程都很困难,因为需要实现正确访问共享变量所需的微妙之处。Go鼓励采用不同的方法,即通过通道传递共享值,并且实际上,这些值永远不会被单独执行的线程主动共享。每次只有一个goroutine可以访问该值。这种方法可能被过度采纳。例如,引用计数可能最好通过在整数变量周围放置互斥锁来完成。但作为高层次的方法,使用通道来控制访问使编写清晰、正确的程序更容易。


非常恰当的答案,附上一篇文章链接。我个人发现TS的方法对于实现数据缓存非常有用,在大多数其他情况下,可以重新设计应用程序以使用通道,这样可以消除很多麻烦。 - Rostyslav Dzinko
2
绝对不应该默认使用"channels"。引用:"使用最具表达力和/或最简单的方式。Go新手会过度使用channels,仅仅因为它是可能的。" 所以"使用最具表达力的方式" - 意味着先思考一下 - Luke
“First”是问题所在。它意味着如果通道工作,就继续前进而不考虑其他选择。两者应该同等考虑。 - Luke
@Luke,如果你认为这两个结构有同等重要性,那么我认为你没有理解 Go 的重点。请阅读我的更新答案,它直接摘自 Go 的网站。 - Ralph Caraveo
1
@Luke,相信我,我理解。我转发这篇文章给 OP 的原因是让他们自己判断。此外,我还提供了我的个人回应,并指向一些其他资源供 OP 阅读。你知道,我已经走过编写同步锁代码的道路。这并不容易,事实上,你应该尝试一下。请不要误引我的话,显然 Mutex 并不难使用。构建一个正确同步的程序是困难的,因此像 Go 这样的语言诞生了。 - Ralph Caraveo
显示剩余3条评论

9

我认为互斥锁对于这个应用程序来说是可以的。将它们包装在一个类型中,以便以后可以更改主意,就像这样。请注意嵌入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"))
}

Playground link


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的解决方案不同,多个读取器可能会相互阻塞。


4
但是,与RWMutex方法不同的是,在这里,即使没有写入者存在,读取者也会相互阻塞,对吗? - alecbz
你能告诉我为什么它们会互相阻塞吗? - Willem

3
  • 你不能直接使用锁来处理消息队列,这就是通道的作用。

  • 你可以通过通道模拟锁的效果,但这并不是通道的本意。

  • 使用锁来保证对共享资源的并发安全访问。

  • 使用通道来进行并发安全的消息排队。

使用RWMutex来保护map写入。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,