竞态条件的赋值_[译] part25: golang Mutex互斥锁

译者:咔叽咔叽

转载请注明出处。

在本教程中,我们将了解互斥锁Mutex。我们还将学习如何使用Mutex和channel解决竞争条件。

临界区

在了解互斥锁之前,先了解并发编程中临界区的概念非常重要。当程序并发运行时,多个Goroutines不应该同时拥有修改共享内存的权限。修改共享内存的这部分代码则称为临界区。例如,假设我们有一段代码将变量x递增1。

x = x + 1

如果上面一段代码被一个Goroutine访问,就不会有任何问题。

让我们看看为什么当有多个Goroutines并发运行时,这段代码会失败。为简单起见,我们假设我们有2个Goroutines并发运行上面的代码行。

上面的代码将按以下步骤执行(有更多技术细节涉及寄存器,如何添加任务等等,但为了本教程的简便,我们假设都是第三步),

获取当前x的值

计算 x + 1

把第二步计算的值赋给x

当这三个步骤仅由一个Goroutine进行时,结果没什么问题。

让我们看看当两个Goroutines并发运行此代码时会发生什么。下图描绘了当两个Goroutines并发访问代码行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,因此在两个Goroutines执行后x为1。

现在让我们看看可能发生的不同情况。

image

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

因此,在这两种情况下,可以看到x的最终值为1或者2,具体取决于协程如何切换。这种程序的输出取决于Goroutines的执行顺序的情况,称为竞态条件。

在上面的场景中,如果在任何时间点只允许一个Goroutine访问临界区,则可以避免竞态条件。这可以通过使用Mutex实现。

Mutex互斥锁

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

sync包中提供了Mutex。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)

}

在上面的程序中,第7行的increment函数将x的值递增1,然后调用WaitGroup上的Done以通知main Goroutine 任务完成。

我们在第15行生成1000个increment Goroutines。这些Goroutines中的每一个都并发运行,当多个Goroutines尝试同时访问x的值,并且计算x + 1时会出现竞态条件。

最好在本地运行此程序,因为playgroud是确定性的不会出现竞态条件。在本地计算机上多次运行此程序,您可以看到由于竞态条件,每次输出都会不同。我遇到的一些输出是final value of x 941, final value of x 928, final value of x 922等等。

使用互斥锁Mutex解决竞态条件

在上面的程序中,我们生成了1000个Goroutines。如果每个都将x的值加1,则x的最终期望值应该为1000。在本节中,我们将使用互斥锁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是一种结构类型,我们在第一行中初始化了一个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

x = x + 1

}

func main() {

var w sync.WaitGroup

ch := make(chan bool, 1)

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

w.Add(1)

go increment(&w, ch)

wg.Done()

}

w.Wait()

fmt.Println("final value of x", x)

}

在上面的程序中,我们创建了一个容量为1的缓冲channel,并将其传递给第18行的increment Goroutine。 此缓冲channel通过将true传递给ch来实现确保只有一个Goroutine访问临界区的。在x递增之前,由于缓冲channel的容量为1,因此尝试写入此channel的所有其他Goroutines都会被阻塞,直到第10行将ch的值取出来。 使用这种方式,实现了只允许一个Goroutine访问临界区。

程序输出,

final value of x 1000

互斥锁Mutex VS 通道channel

我们使用互斥锁Mutex和通道channel解决了竞态条件问题。那么我们怎么决定何时使用哪个?答案在于您要解决的问题。如果您尝试解决的问题更适合互斥锁Mutex,那么请继续使用Mutex。如果需要,请不要犹豫地使用Mutex。如果问题似乎更适合通道channel,那么使用它:)。

大多数Go新手尝试使用channel解决遇到的并发问题,因为它是该语言的一个很酷的功能。这是错的。该语言为我们提供了Mutex或Channel的选项,并且选择任何一种都没有错。

一般情况下,当Goroutine需要相互通信时使用channel,当Goroutine只访问代码的临界区时,使用Mutex。

在我们上面那些问题的情况下,我宁愿使用Mutex,因为这个问题不需要goroutines之间进行任何通信。因此Mutex是一种自然的选择。

我的建议是选择工具去解决问题,而不要为了工具去适应问题:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值