原文地址:https://golangbot.com/mutex/
欢迎来到第二十五节,今天我们将学习如何使用互斥量(Mutexes)和通道(channels)解决竞争问题。
临界区资源--Critical section
在讲解互斥量之前,我们要先理解并发编程中临界区资源的概念。当我们并发的运行程序,多个协程不能同时运行修改资源的那段代码。修改资源的那段代码我们就成为临界区资源。比如我们有一个变量需要增加1:
x = x + 1
如果只是一个协程运行上面的代码自然是没有什么问题的,假入现在有两个协程并发地运行上面的代码,它们执行上面代码时候大致分为3步:
1.获取当前的x值
2.计算x+1
3.重新把2步的值赋给x
下面的图是说明当有两个协程运行上面代码时候会出现什么情况:
假如x初始值为0,协程1初始化x。然后计算x+1,当它把计算后的值重新赋给x之前,系统上下文转到了协程2进行处理,现在线程2初始化x值(此时x仍为0),然后计算x+1,这之后系统上下文又转到协程1,x值被赋值为1,再之后协程2也得到执行,x赋值也是1,所以经过两个协程的计算,x值还是1.
现在让我们看下另一种不同的情况:
在上面的场景中,协程1开始执行并完成了3步运算,所以x结果变成了1,接着协程2开始运行,然后完成所有运算后x值变为2。
从这两个例子你可以看到x的值是1还是2取决于上下文如何切换。这种输出结果无法预期的情况取决于一系列协程的执行没我们称为 竞争条件(race condition).
在上面的例子中,我们可以通过使用互斥量(mutex)解决竞争条件,这样在任意时刻就只有一个协程可以访问临界区资源了。
互斥量--Mutex
互斥量通常被用来提供一个锁定机制去确保在任何时刻只有一个协程访问临界区资源代码,避免发生竞争条件的情况。mutex使用 sync 包。任何代码出现在Lock
和 Unlock
之间就会在任意时刻只能被一个协程执行。
mutex.Lock()
x = x + 1
mutex.Unlock()
在上面代码中,x=x+1在任意时刻只能被一个协程访问执行。
如果一个协程已经对互斥量进行上锁操作,如果一个新的协程也试图使用互斥量进行上锁操作,那么新协程直到互斥量解锁才能重新被锁定。
竞争场景下编码
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)
}
在上面的例子中,increase函数作用就是使x+1,然后调用等待组中的Done()通知它的完成状态。
我们创建了1000个协程,每个都是并发运行,当各个协程试图访问x值的时候就会发生竞争条件了。如果你运行这段代码多次你会发现结果是各不相同,比如:final value of x 941
, final value of x 928
, final value of x 922
等等。
使用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)
}
在playground运行
在上面的例子中,我们使用了一个0值的mutex的结构体类型变量,在increase函数中 x = x + 1
前后增加m.Lock()
和m.Unlock()
,所以现在代码中避免了竞争条件的出现,因为任意时刻只能有一个协程允许访问这句代码。结果打印:
final value of x 1000
传递mutex的地址是必要的,否则协程中会各自拥有自己的mutex一份拷贝,竞争条件依然会出现。
通过channel解决竞争条件
我们也可以通过使用通道来达到解决竞争条件的目的,上代码:
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的缓冲通道,然后传递给increase协程。这个缓冲通道确保只有一个协程可以访问增加x值的临界区资源代码,通过ch <- true 传递给通道一个bool值,因为通道容量为1,所以其他协程试图往通道中写入数据时就被阻塞了,然后知道执行了x+1之后从ch中读取数据后,别的线程才可以继续使用通道。这段代码同样打印:
final value of x 1000
Mutex vs Channels
我们可以通过互斥量和通道都可以解决竞争条件的问题,那么到底什么时候该使用哪种方法呢? 答案就存在问题中,放你的问题如果使用互斥量能更好解决那么不要迟疑就用互斥量,如果使用通道更好一些就使用通道,条条大道通罗马!
大多数新手喜欢使用通道解决每一个并发的问题,因为这种方法看起来更cool一些,这是错误的,语言给了我们使用互斥量和通道的选择,所以无论选择哪个都是没错的。
通常来讲,使用协程需要互相合作时那就选择channel,如果只是需要访问临界区资源代码的话就推荐使用mutex。
我的建议是选择工具去解决问题,而不是为工具解决问题。
本节就到这里,Have a great day.