冰激淋制造商和数据竞态
http://mikespook.com/2014/06/%e7%bf%bb%e8%af%91%e5%86%b0%e6%bf%80%e6%b7%8b%e5%88%b6%e9%80%a0%e5%95%86%e5%92%8c%e6%95%b0%e6%8d%ae%e7%ab%9e%e6%80%81/
Dave Cheney
这是一篇关于数据竞态的文章。本文的相关代码在 Github 上:github.com/davecheney/benandjerry。
这个例子模拟了两个冰激淋制造商 Ben 和 Jerry 随机接待他们的客户。
type IceCreamMaker interface { |
fmt.Printf( "Ben says, \"Hello my name is %s\"\n" , b.name) |
func (j *Jerry) Hello() { |
fmt.Printf( "Jerry says, \"Hello my name is %s\"\n" , j.name) |
var jerry = &Jerry{ "Jerry" } |
var maker IceCreamMaker = ben |
这是数据竞态,傻瓜
大多数程序员应当很容易就看出在这个程序里存在数据竞态。
循环函数在没有加锁的情况下修改了 maker 的值,当主函数中的循环调用 maker.Hello() 的时候,无法明确 Hello 的哪个实现将被调用。
一些程序员可能对此并不在意,Ben 或者 Jerry 来招待客户,到底是哪个无所谓。
让我们运行这个代码,看看会发生什么。
% env GOMAXPROCS=2 go run main.go |
Ben says, "Hello my name is Ben" |
Jerry says, "Hello my name is Jerry" |
Jerry says, "Hello my name is Jerry" |
Ben says, "Hello my name is Jerry" |
Ben says, "Hello my name is Ben" |
等等,这是什么!Ben 有时会认为自己是 Jerry。这怎么可能?
接口值
理解这个竞态的关键是理解接口值在内存中的表现形式。
接口在概念上是一个具有两个字段的结构体。
如果用 Go 来描述接口,它看起来会是这样。
Type 指向实现了用来描述这个接口的值的类型的结构体。Data 指向了值的实现本身。Data 的内容作为被调用方法的接收者,通过接口传递。
通过语句 maker IceCreamMaker = ben,编译器会生成代码做以下事情。
接口的 Type 字段被设置指向 *Ben 类型的定义,而 Data 字段保存了 ben 的副本,一个指向 Ben 的值的指针。
当语句 loop1() 执行的时候,maker = jerry 更新了接口值中的两个字段。
Type 现在指向 *Jerry 的定义,而 Data 保存了指向 Jerry 的指针。
Go 内存模型说向一个机器字写入是原子的,但是接口有两个字大小。当接口值被修改的时候,另外一个 goroutine 可能会读取其内容。在这个例子中,可能会发生
因此 Jerry 的 Hello() 函数调用了 ben 作为接收者。
总结
没有叫做安全数据竞态的东西。你的程序要么没有数据竞态,要么它的操作无法定义。
在这个例子中,Ben 和 Jerry 的内存布局恰好匹配,因此在某些情况下看起来无害。设想如果它们的内存布局不同,会是怎么样的一个混乱世界(这作为练习留给了读者)。
Go 竞态检测器能侦测到这个错误,以及其他可能,只需要简单的在调用 go test、build 或 install 命令时添加 -race 标识。
附加问题
在例子代码里,Hello 方法被定义为 Ben 或 Jerry 的指针接收者。如果替代为在 Ben 或 Jerry 的值上定义的方法,能解决这个数据竞态吗?
扩展阅读
Russ Cox 关于 Go 接口的,在你阅读后,还应当了解下 Russ 撰写的关于这个问题的解释。
Go 竞态检测器的博文(中文翻译)。