[翻译]冰激淋制造商和数据竞态

Dave 总是会给我们带来这种很浅显有趣,又意义深刻的文章。原文在此:Ice cream makers and data races

————翻译分隔线————

冰激淋制造商和数据竞态

Dave Cheney

这是一篇关于数据竞态的文章。本文的相关代码在 Github 上:github.com/davecheney/benandjerry

这个例子模拟了两个冰激淋制造商 Ben 和 Jerry 随机接待他们的客户。

<br />
package main</p>
<p>import &quot;fmt&quot;</p>
<p>type IceCreamMaker interface {<br />
        // 向客户说 Hello<br />
        Hello()<br />
}</p>
<p>type Ben struct {<br />
        name string<br />
}</p>
<p>func (b *Ben) Hello() {<br />
        fmt.Printf(&quot;Ben says, \&quot;Hello my name is %s\&quot;\n&quot;, b.name)<br />
}</p>
<p>type Jerry struct {<br />
        name string<br />
}</p>
<p>func (j *Jerry) Hello() {<br />
        fmt.Printf(&quot;Jerry says, \&quot;Hello my name is %s\&quot;\n&quot;, j.name)<br />
}</p>
<p>func main() {<br />
        var ben = &amp;Ben{&quot;Ben&quot;}<br />
        var jerry = &amp;Jerry{&quot;Jerry&quot;}<br />
        var maker IceCreamMaker = ben</p>
<p>        var loop0, loop1 func()</p>
<p>        loop0 = func() {<br />
                maker = ben<br />
                go loop1()<br />
        }</p>
<p>        loop1 = func() {<br />
                maker = jerry<br />
                go loop0()<br />
        }</p>
<p>        go loop0()</p>
<p>        for {<br />
                maker.Hello()<br />
        }<br />
}<br />

这是数据竞态,傻瓜

大多数程序员应当很容易就看出在这个程序里存在数据竞态。

循环函数在没有加锁的情况下修改了 maker 的值,当主函数中的循环调用 maker.Hello() 的时候,无法明确 Hello 的哪个实现将被调用。

一些程序员可能对此并不在意,Ben 或者 Jerry 来招待客户,到底是哪个无所谓。

让我们运行这个代码,看看会发生什么。

<br />
% env GOMAXPROCS=2 go run main.go<br />
...<br />
Ben says, &quot;Hello my name is Ben&quot;<br />
Jerry says, &quot;Hello my name is Jerry&quot;<br />
Jerry says, &quot;Hello my name is Jerry&quot;<br />
Ben says, &quot;Hello my name is Jerry&quot;<br />
Ben says, &quot;Hello my name is Ben&quot;<br />
...<br />

等等,这是什么!Ben 有时会认为自己是 Jerry。这怎么可能?

接口值

理解这个竞态的关键是理解接口值在内存中的表现形式。

接口在概念上是一个具有两个字段的结构体。

接口的两个字段

如果用 Go 来描述接口,它看起来会是这样。

<br />
type interface struct {<br />
       Type uintptr     // 指向实现接口的类型的指针<br />
       Data uintptr     // 持有实现了接口的接收者的数据<br />
}<br />

Type 指向实现了用来描述这个接口的值的类型的结构体。Data 指向了值的实现本身。Data 的内容作为被调用方法的接收者,通过接口传递。

通过语句 maker IceCreamMaker = ben,编译器会生成代码做以下事情。

指向 Ben

接口的 Type 字段被设置指向 *Ben 类型的定义,而 Data 字段保存了 ben 的副本,一个指向 Ben 的值的指针。

当语句 loop1() 执行的时候,maker = jerry 更新了接口值中的两个字段。

指向 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 竞态检测器的博文中文翻译)。

Leave a Reply

Your email address will not be published. Required fields are marked *