欢迎来到 Golang 系列教程[1]的第 25 篇。

本教程我们学习 Mutex。我们还会学习怎样通过 Mutex 和信道来处理竞态条件(Race Condition)。

临界区

x
x = x + 1

如果只有一个 Go 协程访问上面的代码段,那都没有任何问题。

但当有多个协程并发运行时,代码却会出错,让我们看看究竟是为什么吧。简单起见,假设在一行代码的前面,我们已经运行了两个 Go 协程。

在上一行代码的内部,系统执行程序时分为如下几个步骤(这里其实还有很多包括寄存器的技术细节,以及加法的工作原理等,但对于我们的系列教程,只需认为只有三个步骤就好了):

  1. 获得 x 的当前值
  2. 计算 x + 1
  3. 将步骤 2 计算得到的值赋值给 x

如果只有一个协程执行上面的三个步骤,不会有问题。

x = x + 1

one-scenario
xxx + 1xxx + 1xxxx

现在我们考虑另外一种可能发生的情况。

another-scenario
xxx
x

在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的

Mutex

Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

LockUnlock
mutex.Lock()
x = x + 1
mutex.Unlock()
x = x + 1

如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止。

含有竞态条件的程序

在本节里,我们会编写一个含有竞态条件的程序,而在接下来一节,我们再修复竞态条件的问题。

package main
import (
    "fmt"
    "sync"
    )
var x  = 0
func increment(wg *sync.WaitGroup) {
    x = x + 1
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}
incrementxDone()
incrementxx
final value of x 941final value of x 928final value of x 922

使用 Mutex

xx
package main
import (
    "fmt"
    "sync"
    )
var x  = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

在 playground 中运行[7]

Mutexmincrementxx = x + 1m.Lock()m.Unlock()

于是如果运行该程序,会输出:

final value of x 1000

在第 18 行,传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。

使用信道处理竞态条件

我们还能用信道来处理竞态条件。看看是怎么做的。

package main
import (
    "fmt"
    "sync"
    )
var x  = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
    ch <- true
    x = x + 1
    <- ch
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    ch := make(chan bool, 1)
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

在 playground 中 运行[9]

incrementxxtruex

该程序也输出:

final value of x 1000

Mutex vs 信道

通过使用 Mutex 和信道,我们已经解决了竞态条件的问题。那么我们该选择使用哪一个?答案取决于你想要解决的问题。如果你想要解决的问题更适用于 Mutex,那么就用 Mutex。如果需要使用 Mutex,无须犹豫。而如果该问题更适用于信道,那就使用信道。:)

由于信道是 Go 语言很酷的特性,大多数 Go 新手处理每个并发问题时,使用的都是信道。这是不对的。Go 给了你选择 Mutex 和信道的余地,选择其中之一都可以是正确的。

总体说来,当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。

就我们上面解决的问题而言,我更倾向于使用 Mutex,因为该问题并不需要协程间的通信。所以 Mutex 是很自然的选择。

我的建议是去选择针对问题的工具,而别让问题去将就工具。:)

本教程到此结束。祝你愉快。

下一教程 - 结构体取代类[10]

via: https://golangbot.com/mutex/

作者:Nick Coghlan[11]译者:Noluye[12]校对:polaris1119[13]

本文由 GCTT[14] 原创编译,Go 中文网[15] 荣誉推出

参考资料

[1]

Golang 系列教程: https://studygolang.com/subject/2

[2]

sync: https://golang.org/pkg/sync/

[3]

Mutex: https://tip.golang.org/pkg/sync/#Mutex

[4]

Lock: https://tip.golang.org/pkg/sync/#Mutex.Lock

[5]

Unlock: https://tip.golang.org/pkg/sync/#Mutex.Unlock

[6]

playground: http://play.golang.org

[7]

在 playground 中运行: https://play.golang.org/p/VX9dwGhR62

[8]

Mutex: https://golang.org/pkg/sync/#Mutex

[9]

在 playground 中 运行: https://play.golang.org/p/M1fPEK9lYz

[10]

结构体取代类: https://studygolang.com/articles/12630

[11]

Nick Coghlan: https://golangbot.com/about/

[12]

Noluye: https://github.com/Noluye

[13]

polaris1119: https://github.com/polaris1119

[14]

GCTT: https://github.com/studygolang/GCTT

[15]

Go 中文网: https://studygolang.com/