Go Mutex 互斥锁原理实现
在本篇文章中,我们将了解互斥锁。我们还将学习如何使用互斥锁和通道解决竞争条件。
临界区
在进入互斥锁之前,了解并发编程中临界区的概念很重要。 当一个程序并发运行时,修改共享资源的部分代码不应被多个 Goroutine 同时访问。 这部分修改共享资源的代码称为临界区。 例如,假设我们有一段代码将变量 x 增加 1。
x = x +1
上面这段代码只要是单个 Goroutine 访问就没有问题。
让我们看看为什么当有多个 Goroutines 并发运行时这段代码会失败。 为了简单起见,我们假设我们有 2 个 Goroutine 同时运行上面的代码行。
总结下来,我们假设上面代码执行过程为以下三个步骤
获取 x 的当前值
计算 x + 1
将步骤 2 中的计算值赋给 x
当这三个步骤只由一个 Goroutine 执行时,一切都很好。
让我们讨论当 2 个 Goroutine 同时运行这段代码时会发生什么。 下图描述了当两个 Goroutine 同时访问代码行 x = x + 1
时可能发生的一种情况。
我们假设 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。
下面让我们看一个不同的情况
在上面的场景中,Goroutine 1 开始执行并完成所有三个步骤,因此 x 的值变为 1。然后 Goroutine 2 开始执行。 现在 x 的值为 1,当 Goroutine 2 执行完毕时,x 的值为 2。
所以从这两种情况,你可以看到 x 的最终值是 1 或 2,这取决于上下文切换是如何发生的。 这种程序输出取决于 Goroutines 执行顺序的不良情况称为竞争条件。
在上面的场景中,如果在任何时间点只允许一个 Goroutine 访问代码的临界区,那么竞争条件就可以避免。 这是通过使用互斥锁实现的。
mutex">
互斥锁 Mutex
Mutex 用于提供一种锁机制,以确保在任何时间点只有一个 Goroutine 正在运行代码的临界区,以防止发生竞争条件。
sync
包中提供了互斥锁。 Mutex 上定义了两种方法,即 Lock
和 Unlock
。 在 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 的值。
我们需要在本地运行此程序,因为在线工具是确定性的,并且在线工具运行时不会发生竞争条件。 在本地机器上多次运行这个程序,你可以看到由于竞争条件,每次输出都会不同。 下面是我在本地运行两次的结果
使用互斥锁解决竞争条件
在上面的程序中,我们创建了 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