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

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

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

冰激淋制造商和数据竞态

Dave Cheney

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

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

package main

import "fmt"

type IceCreamMaker interface {
        // 向客户说 Hello 
        Hello()
}

type Ben struct {
        name string
}

func (b *Ben) Hello() {
        fmt.Printf("Ben says, \"Hello my name is %s\"\n", b.name)
}

type Jerry struct {
        name string
}

func (j *Jerry) Hello() {
        fmt.Printf("Jerry says, \"Hello my name is %s\"\n", j.name)
}

func main() {
        var ben = &Ben{"Ben"}
        var jerry = &Jerry{"Jerry"}
        var maker IceCreamMaker = ben

        var loop0, loop1 func()

        loop0 = func() {
                maker = ben
                go loop1()
        }

        loop1 = func() {
                maker = jerry
                go loop0()
        }

        go loop0()

        for {
                maker.Hello()
        }
}

这是数据竞态,傻瓜

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

循环函数在没有加锁的情况下修改了 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 interface struct {
       Type uintptr     // 指向实现接口的类型的指针
       Data uintptr     // 持有实现了接口的接收者的数据
}

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 comment

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