Go语言的Mutex

在本教程中,我们将了解互斥体。我们还将学习如何使用互斥体和通道解决竞争条件。

关键部分

在跳转到互斥体之前,了解并发编程中临界区的概念非常重要。当程序并发运行时,修改共享资源的代码部分不应被多个Goroutines同时访问。这部分修改共享资源的代码称为临界区。例如,假设有一段代码将变量 x 加 1。

x = x + 1  

只要上面的代码是由单个 Goroutine 访问的,就不会有任何问题。

让我们看看为什么当有多个 Goroutines 并发运行时这段代码会失败。为了简单起见,我们假设有 2 个 Goroutine 同时运行上面的代码行。

在内部,系统将按以下步骤执行上述代码行(还有更多涉及寄存器、加法工作原理等技术细节,但为了本教程,我们假设是下面三个步骤),

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

当这三个步骤仅由一个 Goroutine 执行时,一切都很好。

让我们讨论一下当 2 个 Goroutine 同时运行这段代码时会发生什么。下图描述了当两个 Goroutine 同时访问代码行时可能发生的一种情况x = x + 1

Go语言的Mutex_第1张图片

我们假设 x 的初始值为 0。Goroutine 1获取 x 的初始值0,计算 x + 1,在将计算值分配给 x 之前,系统上下文切换到Goroutine 2。现在Goroutine 2获取x其初始值为l 0,进行计算x + 1。之后,系统上下文再次切换到Goroutine 1。现在Goroutine 1将其计算值1赋给x,因此 x 变为 1。然后Goroutine 2再次开始执行,然后将其计算值1赋给x ,因此1x在个2Goroutine 执行之后的结果

现在让我们看看可能发生的不同情况。

Go语言的Mutex_第2张图片

在上述场景中,Goroutine 1开始执行并完成所有三个步骤,因此 x 的值变为1。然后Goroutine 2开始执行。此时 x的值为1,Goroutine 2执行完毕后, 的x值为2

所以从这两种情况,你可以看到x的最终值是12取决于上下文切换是如何发生的。这种程序的输出结果取决于 Goroutine 的执行顺序称为**竞争条件**。

在上述场景中,如果在任何时间点只允许一个 Goroutine 访问代码的关键部分,则可以避免竞争条件。这是通过使用互斥体来实现的。

Mutex

Mutex 用于提供一种锁定机制,以确保在任何时间点只有一个 Goroutine 运行代码的关键部分,以防止竞争条件的发生。

[sync](https://golang.org/pkg/sync/)包中可用。Mutex上定义了两种方法,即Lock和Unlock。调用之间存在的任何代码将仅由一个 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)
}

在上面的程序中,increment的函数x增加1,然后调用WaitGroup的Done()通知其完成。

我们从第 10 行生成 1000 个Goroutine。这些 Goroutine 中的每一个都同时运行,当尝试x增加1 时,就会出现竞争条件。 因为多个 Goroutine 尝试同时访问 x 的值。

您可以看到由于竞争条件,每次的输出都会不同。我遇到的一些输出是

final value of x 981
final value of x 980

使用Mutex解决竞争条件

在上面的程序中,我们生成了 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)
}

Run in playground

Mutex是一种结构类型,我们创建了一个Mutex类型的零值变量m。在上面的程序中,我们更改了increment函数,使 x 递增的代码x = x + 1位于m.Lock()m.Unlock()之间。现在这段代码没有任何竞争条件,因为在任何时间点都只允许一个 Goroutine 执行这段代码。

现在如果运行这个程序,它将输出

final value of x 1000  

在第 21 行传递互斥锁的地址非常重要。如果互斥量是按值传递而不是地址传递,则每个 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)
}

Run in playground

在上面的程序中,我们创建了一个容量为1的缓冲通道,并将其传递给increment。这个缓冲通道用于确保只有一个 Goroutine 访问递增 x 的代码的关键部分。通过缓冲通道传递true到完成的。冲通道的容量为1,所有其他试图写入该通道的 Goroutine 都会被阻塞,直到在第 1 3行读出。实际上,这仅允许一个 Goroutine 访问临界区。

该程序还打印

final value of x 1000  

Mutex与通道

我们已经使用Mutex和通道解决了竞争条件问题。那么我们如何决定何时使用什么?答案就在于你试图解决的问题。如果您尝试解决的问题更适合Mutex,那么请毫不犹豫地使用Mutex。如果问题似乎更适合渠道,那么就使用它。

大多数 Go 新手尝试使用通道来解决每个并发问题,因为这是该语言的一个很酷的功能。这是错误的。该语言为我们提供了使用 Mutex 或 Channel 的选项,选择任何一个都没有错误。

一般来说,当 Goroutine 需要相互通信时使用通道,而当只有一个 Goroutine 应该访问代码的关键部分时使用Mutex。

对于我们上面解决的问题,我更喜欢使用Mutex,因为这个问题不需要 goroutine 之间的任何通信。因此Mutex是一个很好的选择。

我的建议是选择适合问题的工具,而不是试图让问题适合该工具:

本教程到此结束。祝你有美好的一天。

你可能感兴趣的:(Go语言教程,golang,数据库,java)