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 用于提供一种锁机制,以确保在任何时间点只有一个 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
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("最后 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
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("最后 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
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("最后 x 的值为:", x)
}
在上面的程序中,我们创建了一个容量为 1 的缓冲通道,并将其传递给 increment 协程。这个缓冲通道用于确保只有一个 goroutine 访问增加 x 的代码的临界区。 这是通过将 true 传递给缓冲通道来完成的。就在 x 递增之前。 由于缓冲通道的容量为 1,因此所有其他尝试写入该通道的 goroutine 将被阻塞,直到在增加 x 后从该通道读取值。实际上就是只允许一个 goroutine 访问临界区。
运行该程序的结果如下
最后 x 的值为:1000
互斥锁(mutext) vs 通道(channel)
我们已经使用互斥锁和通道解决了竞争条件问题。那么我们如何决定什么时候使用互斥锁,又什么时候使用通道呢?答案在于您要解决的问题。如果您尝试解决的问题更适合使用互斥锁,那么继续使用互斥锁。如果需要,请不要犹豫使用互斥锁。如果问题似乎更适合通道,那就使用它:)。
大多数 go 新手尝试使用通道解决每个并发问题,因为它是该语言的一个很酷的特性。这是错误的。go 语言让我们可以选择使用 mutex 或 channel,选择两者都没有错。
通常,当 goroutine 需要相互通信时使用通道,当只有一个 goroutine 应该访问代码的临界区时使用互斥锁。
对于我们上面解决的问题,我更喜欢使用互斥锁,因为这个问题不需要 goroutine 之间的任何通信。因此互斥将是一个自然的选择。
我的建议是为问题选择工具,不要试图为工具选择问题:)。
转载请发邮件至 1244347461@qq.com 进行申请,经作者同意之后,转载请以链接形式注明出处
本文地址:
相关文章
在 javascript 中验证 google recaptcha 第 2 版
发布时间:2024/03/23 浏览次数:193 分类:javascript
-
本文演示了如何在 javascript 中验证 google recaptcha。
c# 中的 goto 语句
发布时间:2024/02/02 浏览次数:184 分类:编程语言
-
本教程演示了如何在 c# 中使用 goto 以及何时使用它会有所帮助本教程将演示如何在 c# 中使用 goto 语法,并提供一些代码中的实际使用示例。
发布时间:2023/12/20 浏览次数:197 分类:python
-
本文为你提供了 python 中是否存在 goto 语句的答案。本文为你提供了 python 中是否存在 goto 语句的答案。基本上,python 不支持 goto 语句。
避免 python中的 typeerror: input expected at most 1 argument, got 3 错误
发布时间:2023/07/08 浏览次数:671 分类:python
-
python 中避免 typeerror: input expected atmost 1 argument, got 3 error在python编程中,我们有两个内置方法来获取用户的输入:input(prompt)和 raw_input(prompt)。
使用 python 将文件上传到 google 云端硬盘
发布时间:2023/06/15 浏览次数:544 分类:python
-
本文将介绍我们如何使用 python 将文件上传到云盘,以 google drive 为例。 为此,我们将使用 google drive api。
发布时间:2023/05/30 浏览次数:293 分类:python
-
当我们在 numpy 中传递一维数组而不是二维数组时,会发生错误 valueerror: expected 2d array, got 1d array instead 。如您所知,每种编程语言都会遇到很多错误,有些是在运行时,有些是在编译时。 pyth