mutex 和 sync.Once 介绍
假设你的程序中有一个需要某种初始化的功能。该 Bootstrap 程序成本很高,因此将其推迟到实际使用功能的那一刻是有意义的。这样,当功能未激活时,就不会浪费 CPU 周期。 这在 Go 中如何完成?
package main
import "fmt"
var capitals map[string]string
func Bootstrap() {
capitals = make(map[string]string)
capitals["France"] = "Paris"
capitals["Germany"] = "Berlin"
capitals["Japan"] = "Tokyo"
capitals["Brazil"] = "Brasilia"
capitals["China"] = "Beijing"
capitals["USA"] = "Washington"
...
capitals["Poland"] = "Warsaw"
}
func getCapitalCity(country string) string {
if capitals == nil {
Bootstrap()
}
return capitals[country]
}
func main() {
fmt.Println(getCapitalCity("Poland"))
fmt.Println(getCapitalCity("USA"))
fmt.Println(getCapitalCity("Japan"))
}
你可以想象,如果它可以处理所有的国家,其他类似数据库的结构,使用 I/O 操作等,则 bootstrap 函数可能非常昂贵。上述解决方案看起来简单且优雅,但不幸的是,它并不会正常工作。问题在于当 bootstrap 函数在运行时无法阻止其它 Goroutine 做同样的事。而这些繁重的计算只做一次是很可取的。另外在 capitals 刚刚被初始化而其中的 key 还未设置时,其它 Goroutine 会看到它不为 nil 从而尝试从空的 map 中获取值。
sync.Mutex
Go 有包含很多好东西的内置 sync 包。我们可以使用 mutex (mutual exclusion) 来解决我们的同步问题。
import (
"fmt"
"sync"
)
...
var (
capitals map[string]string
mutex sync.Mutex
)
...
func getCapitalCity(country string) string {
mutex.Lock()
if capitals == nil {
Bootstrap()
}
mutex.Unlock()
return capitals[country]
}
bootstrap 程序可被多次运行的问题解决了。如果任何一个 Goroutine 正在运行 bootstrap 或者甚至在判断 capitals == nil
,那么其它 Goroutine 会在 mutex.Lock()
处等待。Unlock 函数一结束另一个 Goroutine 就会被“准许进入”。
但是一次只能有一个 Goroutine 执行被放在 mutex.Lock()
和 mutex.Unlock()
之间的代码。因此如果存放首都城市的 map 被多个 Goroutine 读取,那么一切都会在 if 语句处被处理成一个接一个的。从根本上说对于 map 的读操作(包括判断它是否为 nil)应该允许一次处理多个,因为它是线程安全的。
sync.RWMutex
读 / 写 mutex 可以同时被多个 reader 或者一个 writer 持有(writer 是指改写数据的某种东西):
mutex sync.RWMutex
...
func getCapitalCity(country string) string {
mutex.RLock()
if capitals != nil {
country := capitals[country]
mutex.RUnlock()
return country
}
mutex.RUnlock()
mutex.Lock()
if capitals == nil {
Bootstrap()
}
mutex.Unlock()
return getCapitalCity(country)
}
现在代码变得复杂多了。第一部分使用读锁以允许多个 reader 同时读取 capitals。在一开始 Bootstrap 还未完成,所以调用者会拿到 mutex.Lock()
并且做必要的初始化。当这部分结束,函数可以被再次调用来获取期望的值。这些都在 Bootstrap 函数已经返回之后。
最新的解决方法很显然维护起来比较难。幸运的是有一个内置的方法恰好帮我们应对这种情况……
sync.Once
once sync.Once
...
func getCapitalCity(country string) string {
once.Do(bootstrap)
return capital[country]
}
上面的代码比第一个简明版的解决方法(它并不正常工作)更加简单。可以保证 bootstrap 只会被调用一次,并且从 map 中读数据仅在 bootstrap 返回后才会被执行。
点赞以帮助别人发现这篇文章。如果你想得到新文章的更新,请关注我。
资源
The Go memory model specifies the conditions under which reads of a variable in one Goroutine can be guaranteed to …
golang.org
- sync - Go 编程语言
Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and …
golang.org
Golang Programming Concurrency Synchronization Software Development
喜欢读吗?给 Michał Łowicki 一些掌声吧。
简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。
via: https://medium.com/golangspec/synchronized-goroutines-part-iii-c60bcfeefd2a
作者:Michał Łowicki 译者:krystollia 校对:polaris1119
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
有疑问加站长微信联系(非本文作者))
