[译] part25: golang Mutex 互斥锁

在本教程中,咱们将了解互斥锁Mutex。咱们还将学习如何使用Mutexchannel解决竞态条件。golang

临界区

在了解互斥锁以前,先了解并发编程中临界区的概念很是重要。当程序并发运行时,多个Goroutines不该该同时拥有修改共享内存的权限。修改共享内存的这部分代码则称为临界区。例如,假设咱们有一段代码将变量 x 递增 1。编程

x = x + 1  
复制代码

若是上面一段代码被一个Goroutine访问,就不会有任何问题。并发

让咱们看看为何当有多个Goroutines并发运行时,这段代码会失败。为简单起见,咱们假设咱们有 2 个Goroutines并发运行上面的代码行。函数

上面的代码将按如下步骤执行(有更多技术细节涉及寄存器,如何添加任务等等,但为了本教程的简便,咱们假设都是第三步),工具

  1. 获取当前x的值
  2. 计算 x + 1
  3. 把第二步计算的值赋给x

当这三个步骤仅由一个Goroutine进行时,结果没什么问题。学习

让咱们看看当两个Goroutines并发运行此代码时会发生什么。下图描绘了当两个Goroutines并发访问代码行x = x + 1时可能发生的状况。ui

咱们假设x的初始值为 0。Goroutine 1获取x的初始值,计算x + 1,在它将计算值赋值给x以前,系统切换到Goroutine 2。如今Goroutine 2获取的x的值仍为 0,而后计算x + 1。此时系统再次切回到Goroutine 1。如今Goroutine 1将其计算值 1 赋值给x,所以x变为 1。而后Goroutine 2再次开始执行而后赋值计算值,而后把 1 赋值给x,所以在两个Goroutines执行后x为 1。spa

如今让咱们看看可能发生的不一样状况。code

在上面的场景中,Goroutine 1开始执行并完成全部的三个步骤,所以x的值变为 1。而后Goroutine 2开始执行。如今x的值为 1,当Goroutine 2完成执行时,x的值为 2。cdn

所以,在这两种状况下,能够看到x的最终值为 1 或者 2,具体取决于协程如何切换。这种程序的输出取决于Goroutines的执行顺序的状况,称为竞态条件

在上面的场景中,若是在任什么时候间点只容许一个Goroutine访问临界区,则能够避免竞态条件。这能够经过使用 Mutex 实现。

Mutex互斥锁

Mutex用于提供锁定机制,以确保在任什么时候间点只有一个Goroutine运行代码的临界区,以防止发生竞态条件。

sync包中提供了MutexMutex上定义了两种方法,即锁定LockUnLock。在LockUnLock的调用之间的任何代码将只能由一个Goroutine执行,从而避免竞态条件。

mutex.Lock()  
x = x + 1  
mutex.Unlock()  
复制代码

在上面的代码中,x = x + 1将仅由一个Goroutine执行。

若是一个Goroutine已经持有锁,当一个新的Goroutine试图获取锁的时候,新的Goroutine将被阻塞直到互斥锁被解锁。

拥有竞态条件的程序

在本节中,咱们将编写一个有竞态条件的程序,在接下来的部分中咱们将修复竞态条件。

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)
}
复制代码

Run in playgroud

在上面的程序中,第 7 行的increment函数将x的值递增 1,而后调用WaitGroup上的Done以通知main Goroutine 任务完成。

咱们在第 15 行生成 1000 个increment Goroutines。这些Goroutines中的每个都并发运行,当多个Goroutines尝试同时访问x的值,而且计算x + 1时会出现竞态条件。

最好在本地运行此程序,由于playgroud是肯定性的不会出现竞态条件。在本地计算机上屡次运行此程序,您能够看到因为竞态条件,每次输出都会不一样。我遇到的一些输出是final value of x 941, final value of x 928, final value of x 922等等。

使用互斥锁 Mutex 解决竞态条件

在上面的程序中,咱们生成了 1000 个Goroutines。若是每一个都将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)
}
复制代码

Run in playgroud

Mutex是一种结构类型,咱们在第一行中初始化了一个Mutex类型的变量m。 在上面的程序中,咱们修改了increment函数,使x = x + 1的代码在m.Lock()m.Unlock()之间。如今这段代码没有任何竞态条件,由于在任什么时候候只容许一个Goroutine执行临界区。

如今若是运行该程序,它将输出,

final value of x 1000  
复制代码

在第 18 行传递互斥锁的地址很是重要。若是经过值传递互斥锁而不是地址传递,则每一个Goroutine都将拥有本身的互斥锁副本,那么确定还会发生竞态条件。

使用通道channel解决竞态条件

咱们也可使用channel解决竞态条件。让咱们看看这是如何完成的。

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

Run in playgroud

在上面的程序中,咱们建立了一个容量为 1 的缓冲channel,并将其传递给第 18 行的increment Goroutine。 此缓冲channel经过将true传递给ch来实现确保只有一个Goroutine访问临界区的。在x递增以前,因为缓冲channel的容量为 1,所以尝试写入此channel的全部其余Goroutines都会被阻塞,直到第 10 行将ch的值取出来。 使用这种方式,实现了只容许一个 Goroutine 访问临界区。

程序输出,

final value of x 1000  
复制代码

互斥锁Mutex VS 通道channel

咱们使用互斥锁Mutex和通道channel解决了竞态条件问题。那么咱们怎么决定什么时候使用哪一个?答案在于您要解决的问题。若是您尝试解决的问题更适合互斥锁Mutex,那么请继续使用Mutex。若是须要,请不要犹豫地使用Mutex。若是问题彷佛更适合通道channel,那么使用它:)。

大多数 Go 新手尝试使用channel解决遇到的并发问题,由于它是该语言的一个很酷的功能。这是错的。该语言为咱们提供了MutexChannel的选项,而且选择任何一种都没有错。

通常状况下,当Goroutine须要相互通讯时使用channel,当Goroutine只访问代码的临界区时,使用Mutex

在咱们上面那些问题的状况下,我宁愿使用Mutex,由于这个问题不须要goroutines之间进行任何通讯。所以Mutex是一种天然的选择。

个人建议是选择工具去解决问题,而不要为了工具去适应问题:)

相关文章
相关标签/搜索