首页 > Go语言教程 > Go语言并发 阅读:2,453

Go语言竞争状态简述

Go语言中如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。

竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。

【示例】包含竞争状态的示例程序。
// 这个示例程序展示如何在程序里造成竞争状态
// 实际上不希望出现这种情况
package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    // counter 是所有goroutine 都要增加其值的变量
    counter int

    // wg 用来等待程序结束
    wg sync.WaitGroup
)

// main 是所有Go 程序的入口
func main() {
    // 计数加 2,表示要等待两个goroutine
    wg.Add(2)

    // 创建两个goroutine
    go incCounter(1)
    go incCounter(2)

    // 等待 goroutine 结束
    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

// incCounter 增加包里counter 变量的值
func incCounter(id int) {
    // 在函数退出时调用Done 来通知main 函数工作已经完成
    defer wg.Done()

    for count := 0; count < 2; count++ {
        // 捕获 counter 的值
        value := counter

        // 当前 goroutine 从线程退出,并放回到队列
        runtime.Gosched()

        // 增加本地value 变量的值
        value++

        // 将该值保存回counter
        counter = value
    }
}
输出结果如下所示。

Final Counter: 2

变量 counter 会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时,counter 变量的值为 2。下图提供了为什么会这样的线索。

竞争状态下程序行为的图像表达
图:竞争状态下程序行为的图像表达

每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在goroutine 切换的时候。每个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候,counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。

代码说明如下:
  • 在第 25 行和第 26 行,使用 incCounter 函数创建了两个 goroutine。
  • 在第 34 行,incCounter 函数对包内变量 counter 进行了读和写操作,而这个变量是这个示例程序里的共享资源。每个 goroutine 都会先读出这个 counter 变量的值。
  • 在第 40 行将 counter 变量的副本存入一个叫作 value 的本地变量。
  • 在第 46 行,incCounter 函数对 value 的副本的值加 1。
  • 在第 49 行将这个新值存回到 counter 变量。

这个函数在第 43 行调用了 runtime 包的 Gosched 函数,用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。

Go语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的示例代码,代码如下所示。
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
==================
WARNING: DATA RACE
Write by goroutine 5:
    main.incCounter()
        /example/main.go:49 +0x96
Previous read by goroutine 6:
    main.incCounter()
        /example/main.go:40 +0x66
Goroutine 5 (running) created at:
    main.main()
        /example/main.go:25 +0x5c
Goroutine 6 (running) created at:
    main.main()
        /example/main.go:26 +0x73
==================
Final Counter: 2
Found 1 data race(s)
上述代码中的竞争检测器指出了 4 行代码有问题,如下所示。

Line 49: counter = value
Line 40: value := counter
Line 25: go incCounter(1)
Line 26: go incCounter(2)

上面展示了竞争检测器查到的哪个 goroutine 引发了数据竞争,以及哪两行代码有冲突。毫不奇怪,这几行代码分别是对 counter 变量的读和写操作。一种修正代码、消除竞争状态的办法是,使用 Go语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。