Go Mutex 互斥锁原理实现

在本篇文章中,我们将了解互斥锁。我们还将学习如何使用互斥锁和通道解决竞争条件。

临界区

在进入互斥锁之前,了解并发编程中临界区的概念很重要。 当一个程序并发运行时,修改共享资源的部分代码不应被多个 Goroutine 同时访问。 这部分修改共享资源的代码称为临界区。 例如,假设我们有一段代码将变量 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 多协程执行加法运算

Go 多协程执行加法运算

 

我们假设 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,因此在两个 Goroutine 执行后 x 是 1。

下面让我们看一个不同的情况

 

Go 多协程顺序执行

Go 多协程顺序执行

 

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

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

在上面的场景中,如果在任何时间点只允许一个 Goroutine 访问代码的临界区,那么竞争条件就可以避免。 这是通过使用互斥锁实现的。

mutex">

互斥锁 Mutex

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

sync 包中提供了互斥锁。 Mutex 上定义了两种方法,即 LockUnlock。 在 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

funcincrement(wg *sync.WaitGroup) {

x = x + 1

wg.Done()

}

funcmain() {

var w sync.WaitGroup

for i := 0; i < 1000; i++ {

w.Add(1)

go increment(&w)

}

w.Wait()

fmt.Println("最后 x 的值为:", x)

}

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

我们生成了 1000 个 increment Goroutine。 上述程序这些 Goroutines 中同时运行,并且在尝试增加 x 时会发生竞争。 因为多个 Goroutine 尝试同时访问 x 的值。

我们需要在本地运行此程序,因为在线工具是确定性的,并且在线工具运行时不会发生竞争条件。 在本地机器上多次运行这个程序,你可以看到由于竞争条件,每次输出都会不同。 下面是我在本地运行两次的结果

Go 竞争条件不同的运行结果

使用互斥锁解决竞争条件

在上面的程序中,我们创建了 1000 个 Goroutine。 如果每次都将 x 的值增加 1,则 x 的最终期望值应该是 1000。在本节中,我们将使用互斥锁解决上述程序中由于竞争条件造成的错误结果。

package main

import (

"fmt"

"sync"

)

var x = 0

funcincrement(wg *sync.WaitGroup, m *sync.Mutex) {

m.Lock()

x = x + 1

m.Unlock()

wg.Done()

}

funcmain() {

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("最后 x 的值为:", x)

}

运行示例

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

现在运行这个程序,它会输出如下结果

最后 x 的值为:1000

在上面程序中传递互斥锁的地址很重要。如果互斥量是按值传递而不是地址传递,每个 Goroutine 都会有自己的互斥量副本,竞争条件仍然会发生。

使用 Channel 解决竞争条件

我们也可以使用通道解决竞争条件。

package main

import (

"fmt"

"sync"

)

var x = 0

funcincrement(wg *sync.WaitGroup, ch chanbool) {

ch <- true

x = x + 1

<- ch

wg.Done()

}

funcmain() {

var w sync.WaitGroup

ch := make(chanbool, 1)

for i := 0; i < 1000; i++ {

w.Add(1)

go increment(&w, ch)

}

w.Wait()

fmt.Println("最后 x 的值为:", x)

}

运行示例

在上面的程序中,我们创建了一个容量为 1 的缓冲通道,并将其传递给 increment 协程。这个缓冲通道用于确保只有一个 Goroutine 访问增加 x 的代码的临界区。 这是通过将 true 传递给缓冲通道来完成的。就在 x 递增之前。 由于缓冲通道的容量为 1,因此所有其他尝试写入该通道的 Goroutine 将被阻塞,直到在增加 x 后从该通道读取值。实际上就是只允许一个 Goroutine 访问临界区。

运行该程序的结果如下

最后 x 的值为:1000

互斥锁(Mutext) vs 通道(Channel)

我们已经使用互斥锁和通道解决了竞争条件问题。那么我们如何决定什么时候使用互斥锁,又什么时候使用通道呢?答案在于您要解决的问题。如果您尝试解决的问题更适合使用互斥锁,那么继续使用互斥锁。如果需要,请不要犹豫使用互斥锁。如果问题似乎更适合通道,那就使用它:)。

大多数 Go 新手尝试使用通道解决每个并发问题,因为它是该语言的一个很酷的特性。这是错误的。Go 语言让我们可以选择使用 Mutex 或 Channel,选择两者都没有错。

通常,当 Goroutine 需要相互通信时使用通道,当只有一个 Goroutine 应该访问代码的临界区时使用互斥锁。

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

我的建议是为问题选择工具,不要试图为工具选择问题:)。

本文转载自:迹忆客(https://www.jiyik.com)

以上是 Go Mutex 互斥锁原理实现 的全部内容, 来源链接: utcz.com/z/290247.html

回到顶部