在本教程中,咱们将了解互斥锁。咱们还将学习如何使用互斥锁和通道解决竞争问题。编程
在讲互斥锁以前,了解并发编程中临界区的概念很是重要。当程序同时运行时,多个 goroutine 同时访问修改共享资源,修改共享资源的这段代码称为临界区。例如,假设咱们要将变量 x 递增1.并发
x = x + 1
只要上面的代码被一个 goroutine 访问,就不会有任何问题。函数
<!-- more -->工具
让咱们看看为何当有多个 goroutine 同时运行时,这段代码会失败。为简单起见,咱们假设有2个 goroutine 同时运行上面的代码行。学习
在内部,上面的代码行将由系统按下面的步骤执行。spa
当这三个步骤仅由一个 goroutine 进行时,一切都很顺利。code
让咱们讨论当2个 goroutine 同时运行此代码会发生什么。下图描绘了两个 goroutine 同时访问代码行时可能发生的状况。cdn
图中,第一步协程1当前x值是0,计算x+1
,而后系统切换上下文到协程2,第二步,协程2当前x值是0,计算x+1
,这时系统又切换上下文到协程1,进行分配x值,而后又切换上下文到协程2,进行分配x值,最后,x的值仍是1.协程
让咱们再看看可能发生的不一样状况:blog
在上面的场景中,协程1开始执行并完成3个步骤,这时x值是1,而后开始执行协程2,如今x的值已是1了,在协程2执行完成,x的值就是2了。
所以,在这两种状况下,你能够看到 x 的最终值是1或2取决于上下文切换的方式。这种类型的不良状况,其中程序的输出取决于 goroutine 的执行顺序,称为竞争条件。
为了不竞争条件,能够经过使用 Mutex 实现。
Mutex 用于提供锁定机制,以确保在任什么时候间点只有一个 goroutine 在临界区运行,已防止发生竞争条件。
sync
包中提供了 Mutex。Mutex 定义了两个方法,即 Lock
和 Unlock
,在 Lock
和Unlock
之间将仅由一个 goroutine 被执行,从而避免了竞争条件。
mutex.Lock() x = x + 1 mutex.Unlock()
在上面的代码中,x=x+1
将在任什么时候间点仅由一个 goroutine 执行,从而防止竞争条件。
若是一个 goroutine 已经 Lock
,若是一个新的 goroutine 试图 Lock
,新的 goroutine 将会阻塞,直到 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) }
请在本地运行此程序,由于操做是肯定性的,操做上不会出现比赛条件。在本地计算机上屡次运行此程序,您能够看到因为竞争条件,每次输出都会有所不一样。其中一些我所遇到的产出是final value of x 941
,final value of x 928
,final value of x 922
等。
在上面的程序中,咱们产生了1000个Goroutines。若是每一个都将x的值递增1,则x的最终指望值应为1000.在本节中,咱们将使用互斥锁修复上述程序中的竞争条件。
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是一个结构类型,在第 15 行咱们建立了一个零值的变量m的Mutex类型。在上面的程序中,咱们更改了increment
函数,以便增长x的代码x = x + 1
在m.Lock()
和m.Unlock()
之间。如今这段代码没有任何竞争条件,由于在任什么时候候只容许一个Goroutine执行这段代码。
咱们也可使用通道解决竞争条件。让咱们看看如何实现的。
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
的Goroutine。此缓冲通道用于确保只有一个Goroutine访问增长x的代码的关键部分。这是经过传递true到第 8行号中的缓冲通道来完成的,而后 x增长。因为缓冲通道的容量为1,全部其余尝试写入此通道的Goroutines都会被阻塞,直到在第9行增长x后从该通道读取该值。实际上,这只容许一个Goroutine访问临界区。
这个程序也打印
final value of x 1000
咱们使用互斥锁和通道解决了竞争条件问题。那么咱们如何决定什么时候使用呢?答案在于你要解决的问题。若是你解决的问题更合适互斥锁,那么请继续使用互斥锁。若是须要,请不要犹豫使用互斥锁。若是问题更适合通道,那么使用它:)(没有银弹)
大多数 Go 新手尝试使用通道解决每一个并发问题,由于它是该语言的一个很酷的功能。这是错误的,语言为咱们提供了使用 Mutex 和 Channel 的选择,而且选择任何一种都没有错。
通常状况下,当 goroutine 须要互相通讯时使用通道,当只有一个 goroutine 应该访问代码的临界区时使用互斥。
在咱们上面的问题状况下,我宁愿使用互斥锁,由于这个问题不须要 goroutine 之间任何通讯。所以互斥锁是一种天然的选择。
个人建议是根据问题选择工具,不要试图让问题适应工具。