[Go 教程系列笔记] Mutex(互斥锁)

在本教程中,咱们将了解互斥锁。咱们还将学习如何使用互斥锁和通道解决竞争问题。编程

临界区

在讲互斥锁以前,了解并发编程中临界区的概念很是重要。当程序同时运行时,多个 goroutine 同时访问修改共享资源,修改共享资源的这段代码称为临界区。例如,假设咱们要将变量 x 递增1.并发

x = x + 1

只要上面的代码被一个 goroutine 访问,就不会有任何问题。函数

<!-- more -->工具

让咱们看看为何当有多个 goroutine 同时运行时,这段代码会失败。为简单起见,咱们假设有2个 goroutine 同时运行上面的代码行。学习

在内部,上面的代码行将由系统按下面的步骤执行。spa

  1. 获取 x 的当前值
  2. 计算 x+1
  3. 将步骤2中计算的值分配给 x

当这三个步骤仅由一个 goroutine 进行时,一切都很顺利。code

让咱们讨论当2个 goroutine 同时运行此代码会发生什么。下图描绘了两个 goroutine 同时访问代码行时可能发生的状况。cdn

cs5

图中,第一步协程1当前x值是0,计算x+1,而后系统切换上下文到协程2,第二步,协程2当前x值是0,计算x+1,这时系统又切换上下文到协程1,进行分配x值,而后又切换上下文到协程2,进行分配x值,最后,x的值仍是1.协程

让咱们再看看可能发生的不一样状况:blog

cs-6

在上面的场景中,协程1开始执行并完成3个步骤,这时x值是1,而后开始执行协程2,如今x的值已是1了,在协程2执行完成,x的值就是2了。

所以,在这两种状况下,你能够看到 x 的最终值是1或2取决于上下文切换的方式。这种类型的不良状况,其中程序的输出取决于 goroutine 的执行顺序,称为竞争条件

为了不竞争条件,能够经过使用 Mutex 实现。

Mutex 互斥

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

sync 包中提供了 Mutex。Mutex 定义了两个方法,即 LockUnlock,在 LockUnlock 之间将仅由一个 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 941final value of x 928final 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 + 1m.Lock()m.Unlock() 之间。如今这段代码没有任何竞争条件,由于在任什么时候候只容许一个Goroutine执行这段代码。

使用 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的通道,并将其传递给increment的Goroutine。此缓冲通道用于确保只有一个Goroutine访问增长x的代码的关键部分。这是经过传递true到第 8行号中的缓冲通道来完成的,而后 x增长。因为缓冲通道的容量为1,全部其余尝试写入此通道的Goroutines都会被阻塞,直到在第9行增长x后从该通道读取该值。实际上,这只容许一个Goroutine访问临界区。

这个程序也打印

final value of x 1000

Mutex vs Channel

咱们使用互斥锁和通道解决了竞争条件问题。那么咱们如何决定什么时候使用呢?答案在于你要解决的问题。若是你解决的问题更合适互斥锁,那么请继续使用互斥锁。若是须要,请不要犹豫使用互斥锁。若是问题更适合通道,那么使用它:)(没有银弹)

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

通常状况下,当 goroutine 须要互相通讯时使用通道,当只有一个 goroutine 应该访问代码的临界区时使用互斥。

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

个人建议是根据问题选择工具,不要试图让问题适应工具。

相关文章
相关标签/搜索