为什么竞争检测器没有检测到这个竞争条件?

3

我正在学习Go编程语言,并且现在正在探索 atomic 包。

在这个例子中,我正在创建一些 Goroutines,它们都需要增加一个包级别的变量。避免竞争条件有几种方法,但是现在我想使用 atomic 包来解决这个问题。

当我在 Windows 电脑上运行以下代码 (go run main.go) 时,结果并不是我期望的 (我期望最终结果为1000)。最终数字在 900 和 1000 之间。在 Go Playground 中运行代码时,该值为 1000。

以下是我使用的代码: https://play.golang.org/p/8gW-AsKGzwq

var counter int64
var wg sync.WaitGroup

func main() {
    num := 1000
    wg.Add(num )
    for i := 0; i < num ; i++ {
        go func() {
            v := atomic.LoadInt64(&counter)
            v++
            atomic.StoreInt64(&counter, v)

            // atomic.AddInt64(&counter, 1)

            // fmt.Println(v)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("final", counter)
}

go run main.go
final 931

go run main.go
final 960

go run main.go
final 918

我本以为竞态检测器会报错,但它没有:

go run -race main.go
final 1000

它输出了正确的值(1000)。

我正在使用 go version go1.12.7 windows/amd64 (此时最新版本)

我的问题:

  • 为什么竞态检测器没有报错,但我在不使用竞态检测器运行代码时看到了不同的值?
  • 我认为 Load/Store 组合无法正常工作的原因是两个 atomic 调用不是作为一个整体进行原子操作。在这种情况下,我应该使用 atomic.AddInt64 方法,是这样吗?

任何帮助都将不胜感激 :)


3
这是一个很好的例子,说明原子操作并不提供同步,它们只提供安全的并发访问。 - JimB
1个回答

6

您的代码里没有并发问题,所以竞态检测器检测不到任何问题。您的counter变量总是通过启动的goroutine使用atomic包进行访问,而不是直接访问。

为什么有时会得到1000,有时会得到较少的数字呢?这是由运行goroutines的活动线程数所决定:GOMAXPROCS。在Go Playground中,此值为1,因此任何时候只有一个活动的goroutine(所以基本上您的应用程序是按顺序执行的,没有任何并行性)。目前的goroutine调度器不会随意挂起goroutines。

在您的本地计算机上,您可能拥有多核CPU,并且GOMAXPROCS默认为可用逻辑CPU的数量,因此GOMAXPROCS大于1,因此您有多个goroutine并行运行(真正的并行,而不仅仅是并发)。

请参见以下片段:

v := atomic.LoadInt64(&counter)
v++
atomic.StoreInt64(&counter, v)

你加载 counter 的值并将其赋给 v,你增加 v,然后将增加后的 v 的值存储回去。如果两个并行的 goroutine 同时做了这件事情,会发生什么呢?假设它们同时加载值 100。它们都增加本地副本:101。它们都写回 101,即使应该是 102
是的,原子方式增加计数器的正确方法是使用 atomic.AddInt64() 像这样:
for i := 0; i < num; i++ {
    go func() {
        atomic.AddInt64(&counter, 1)
        wg.Done()
    }()
}

这样你将始终获得1000,无论 GOMAXPROCS 是多少。


即使GOMAXPROCS为1,为什么1000个并发线程不会导致竞争条件呢?比如counter=100,线程1将counter加载到v中,将其增加,所以v=101。然后进行上下文切换,线程2将counter加载到v中,将其增加,所以v=101,并将101存储在内存中,然后再次切换回线程1,它也将101存储下来。 - mangusta
3
从理论上讲,即使使用一个线程也可能获得少于1000的结果,但当前的goroutine调度程序不会随意使goroutine进入park状态。因此,您预期的“上下文切换”并不存在。 - icza
1
你唯一能做的假设就是语言和内存模型规范中提出的那些。编译器可以在该模型内自由优化操作顺序。没有同步机制提供任何“发生于”关系,因此代码和运行时可以根据规范重新排序操作,即使在单个线程内也是如此。 - JimB
1
我明白了。不过,"-race" 工具在所描述的情况下必须提供一些警告。最终用户可能不知道 GOMAXPROCS 的值。毕竟,程序偶尔产生除 1000 以外的输出事实上就是一个 100% 的竞态条件。 - mangusta
3
@mangusta说,竞态检测器仅能检测数据竞争,而这绝对不是一种数据竞争。你可以将其视为某种“逻辑上”的竞争,但程序的执行方式完全符合意图。事实上,即使在所有 goroutine 在 load 和 store 之间的同一点上都进行了 yield,1 也是有效的输出结果。 - JimB
显示剩余2条评论

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