在说到锁mutex之前,先要理解并发编程中临界区的概念。当程序并发运行时,修改共享资源的那部分代码不应该被多个Goroutines访问,这部分修改共享资源的代码称为临界区。假定我们有如下一行代码:
x = x 1
如果上面的代码段被单个Goroutine访问,不会出现任何问题。让我们看看为什么当有多个Goroutines同时运行时,这段代码会失败。为了简单起见,我们假设我们有2个Goroutines同时运行上述代码。系统将在以下步骤中执行上述代码行(还有更多涉及寄存器、加法如何工作等的技术细节,让我们假设这是三个步骤):
1、获取 x 的当前值;
2、计算 x 1;
3、将步骤2中的计算值赋给 x;
当这三个步骤仅由一个Goroutine执行时,一切都很好。
让我们讨论一下当 2 个Goroutines同时运行此代码时会发生什么。下图描述了当两个 Goroutines 同时访问代码行 x = x 1 时可能发生的情况。
我们假设 x 的初始值为 0。Goroutine 1 获取 x 的初始值,计算 x 1,在将计算值分配给 x 之前,系统上下文切换到 Goroutine 2。现在 Goroutine 2 得到 x 的初始值,它仍然是 0,计算 x 1。在此之后,系统上下文再次切换到 Goroutine1。现在 Goroutine 1 将其计算值 1 分配给 x,因此 x 变为 1。然后 Goroutine2 再次开始执行,然后分配其计算值,该值再次为 1 到 x,因此在两个 Goroutines 执行后 x 为 1。
现在让我们看看可能发生的另外不同情况:
在上述场景中,Goroutine 1 开始执行并完成其所有三个步骤,因此 x 的值变为 1。然后 Goroutine 2 开始执行。现在 x 的值是 1,当 Goroutine 2 完成执行时,x 的值是 2。
因此,从这两种情况下,您可以看到 x 的最终值是 1 或 2,具体取决于上下文切换的发生方式。这种程序的输出取决于 Goroutines 执行顺序的不良情况称为竞争条件。
在上述场景中,如果只允许一个 Goroutine 在任何时间点访问代码的关键部分,则可以避免竞争条件。这是通过使用互斥锁实现的。
Mutex互斥锁Mutex用于提供锁定机制,以确保在任何时间点只有一个 Goroutine 运行代码的临界区,以防止竞争条件发生。
互斥锁Mutex在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("final value of x", x)
}
在上面的程序中,increment函数用于变量x递增计算,之后调用WaitGroup的Done()函数表明运行完成。
我们生成 1000 个increment的Goroutine。这些 Goroutines 中的每一个都同时运行,当尝试递增 x时,会发生竞争条件,因为多个 Goroutines 尝试同时访问 x 的值。
可以在本地运行此程序,多次运行此程序,您可以看到由于竞争条件,每次的输出都会有所不同。我遇到的一些输出是 x 941 的最终值、x 928 的最终值、x 922 的最终值等等。
使用Mutex解决竞争条件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是一种struct类型,在上面的程序中,我们创建一个Mutex类型的零值变量m,更改了increment函数,使递增x = x 1的代码介于m.Lock()和m.Unlock()之间。现在这段代码没有任何竞争条件,因为只允许一个 Goroutine 在任何时间点执行这段代码。
如果程序运行,会得到如下输出:
final value of x 1000
在第18行传递互斥锁的地址非常重要,如果互斥锁是按值传递而不是传递地址,则每个 Goroutine 都有自己的互斥锁副本,并且仍然会发生竞争条件。
使用Channel解决竞争条件也可以用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的缓冲通道,并将其传递给第18行中的增量 Goroutine。此缓冲通道用于确保只有一个Goroutine 访问递增 x 的代码的临界区。这是通过在 x 递增之前将 true传递给第8行中的缓冲通道来完成的。由于缓冲通道的容量为 1,因此所有其他尝试写入此通道的 Goroutines 都将被阻塞,直到在第10行中递增 x 后从该通道读取该值。实际上,这只允许一个 Goroutine 访问临界区。
如果程序运行,会得到如下输出:
final value of x 1000
Mutex vs Channels
我们已经使用Mutex和Channel解决了竞争条件问题。那么我们如何决定何时使用什么呢?答案在于您要解决的问题。如果您尝试解决的问题更适合互斥锁,请继续使用互斥锁。如果需要,不要犹豫,使用互斥锁。如果问题似乎更适合通道,那么:)使用它。
大多数 Go 新手尝试使用通道解决每个并发问题,因为它是该语言的一个很酷的功能。这是错误的。Go为我们提供了使用Mutex或Channel的选项,选择两者都没有错。
通常,当 Goroutines 需要相互通信时使用通道,当只有一个 Goroutine 应该访问代码的临界区时,使用互斥锁。
对于我们上面解决的问题,更喜欢使用互斥锁,因为这个问题不需要goroutine之间的任何通信。因此,互斥锁将是自然而然的选择。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved