Go中Mutex和Channel使用哪个?

Go中Mutex和Channel使用哪个?

首页动作格斗访问代码零更新时间:2024-04-25
临界区

在说到锁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