欢迎来到 Golang 系列教程[1]的第 25 篇。
本教程我们学习 Mutex。我们还会学习怎样通过 Mutex 和信道来处理竞态条件(Race Condition)。
临界区
x
x = x + 1
如果只有一个 Go 协程访问上面的代码段,那都没有任何问题。
但当有多个协程并发运行时,代码却会出错,让我们看看究竟是为什么吧。简单起见,假设在一行代码的前面,我们已经运行了两个 Go 协程。
在上一行代码的内部,系统执行程序时分为如下几个步骤(这里其实还有很多包括寄存器的技术细节,以及加法的工作原理等,但对于我们的系列教程,只需认为只有三个步骤就好了):
获得 x 的当前值 计算 x + 1 将步骤 2 计算得到的值赋值给 x
如果只有一个协程执行上面的三个步骤,不会有问题。
x = x + 1
xxx + 1xxx + 1xxxx
现在我们考虑另外一种可能发生的情况。
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] 荣誉推出
参考资料
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/