原文来自:https://golangbot.com/mutex/
本节我们将学习互斥锁。我们将学习如何利用互斥量mutexes和通道channel解决竞争问题。
临界区
在进入互斥锁之前,我们需要先了解程序中临界区的概念。当程序并发时,修改共享资源的代码不能够同时被多个协程同时修改。修改共享资源部分的代码就叫做临界区。例如:假设我们有一行代码给变量x加1:
x = x + 1
如果只有一个协程访问上述代码,那么没有任何问题。
让我们来看看为什么在多个协程并发运行时代码会出问题。为了方便起见,我们假设有2个协程会运行上述那行代码。
实际上,x=x+1在操作系统内部执行时是有多个步骤的。更多技术问题涉及到寄存器等其他运算。在这里我们简单归纳为三个步骤:
- 获取当前x的值
- 计算x+1
- 将计算结果赋值给x
当只有一个协程运行时,3个步骤没有问题。
当有2个协程同时运行时,下图中描述了两个协程同时并发执行x=x+1的一种场景。
我们假设x的初始值为0,协程1先获取初始值x,然后计算x+1。在将计算结果赋值给x之前,系统调度协程2执行。现在协程2获得x的初始值仍然时0,计算x+1.然后系统调度协程1执行将计算结果1赋值给x,因此x变为1.之后协程2执行,同样将计算结果1赋值给x。因此在两个协程执行结束后x最终的值是1.
让我们在看另一种可能的场景。
在上诉场景中,协程1先执行而且一次性执行完三个步骤,因此x的值变成1.之后协程2开始执行,这个时候协程2获取的x的初始值变成1,当协程2执行结束,x的值变成2.
从上述两个例子,我们可以看到最终x的值是1或者2取决于上下文如何切换。程序结果无法预期,结果取决于协程的调度顺序,我们称之为竞争条件。
上述场景,在只有一个协程在同一时刻被允许访问临界区资源,竞争条件是不会发生的。我们可以使用互斥锁mutex来解决竞争条件。
互斥量
互斥量提供了一种锁机制来保证在任何时刻只有一个协程执行临界区代码,防止竞争条件发生。
golang的sync包提供互斥量mutex。互斥量定义了两种方法:Lock和Unlick。任何在Lock和unlock之间的代码只能被一个协程执行,从而避免竞争条件发生。例如下面代码x=x+1,同一时刻只会被一个协程调用。
mutex.Lock()
x = x + 1
mutex.Unlock()
如果已经有一个协程持有互斥锁未释放,这个时候另外一个协程尝试去请求该互斥锁,该协程会一直阻塞直到原协程释放互斥锁。
竞争条件的程序
在这一节,我们将写一个带有竞争条件的程序。之后的课程将会解决这个竞争问题。
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)
}
上述代码,函数increment的功能是把x+1,然后调用Done()通知协程执行结束。
main协程生成1000个increment协程并发执行。当多个协程并发执行试图修改x的值时发生竞争条件。
运行这个程序将会有多种不通的结果。可以自己去运行试试。
互斥量解决竞争条件问题
上述代码,如果每个increment协程都令x的值加1,那么x的期望结果是1000.这一节,我们将利用互斥量mutex来解决这个问题。
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)
}
Mutex是一个结构体,我们新建一个初始值为0的互斥量变量m。我们修改函数increment,将x=x+1放置在m.Lock()和m.Unlock(),这样,这部分在任何时刻只允许被一个协程所执行,避免竞争发生。
程序执行结果将打印:
final value of x 1000
注意:代码18行,传递给increment函数的是互斥量的指针,这样每个协程中的互斥量是同一个变量,从而达到互斥的效果。如果传递的参数是m,那么传递给每个increment协程的是互斥量m的一个副本,而不是m本身,这样达不到互斥量的结果。
利用通道解决竞争条件
我们也可以通过通道来解决竞争问题。
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)
}
上述程序,我们创建一个缓存容量为1的缓冲通道,并将它传递给协程increment。缓冲通道保证只有一个协程可以访问临界区资源。
通过把数据true写入缓冲通道,由于通道容量是1,其他协程试图往通道写数据时被阻塞,除非值从通道中被读出。
互斥量和通道
我们可以通过互斥量和通道都可以解决竞争条件的问题,那么到底什么时候该使用哪种方法呢? 答案就存在问题中,放你的问题如果使用互斥量能更好解决那么不要迟疑就用互斥量,如果使用通道更好一些就使用通道,条条大道通罗马!
大多数新手喜欢使用通道解决每一个并发的问题,因为这种方法看起来更cool一些,这是错误的,语言给了我们使用互斥量和通道的选择,所以无论选择哪个都是没错的。
通常来讲,使用协程需要互相合作时那就选择channel,如果只是需要访问临界区资源代码的话就推荐使用mutex。
我的建议是选择工具去解决问题,而不是为工具解决问题。
本节就到这里,Have a great day.
ps:新手翻译,欢迎大家指正!