[Go 教程系列笔记] Mutex(互斥锁)

在本教程中,我们将了解互斥锁。我们还将学习如何使用互斥锁和通道解决竞争问题。

临界区

在讲互斥锁之前,了解并发编程中临界区的概念非常重要。当程序同时运行时,多个 goroutine 同时访问修改共享资源,修改共享资源的这段代码称为临界区。例如,假设我们要将变量 x 递增1.

1
x = x + 1

只要上面的代码被一个 goroutine 访问,就不会有任何问题。

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

在内部,上面的代码行将由系统按下面的步骤执行。

  1. 获取 x 的当前值
  2. 计算 x+1
  3. 将步骤2中计算的值分配给 x

当这三个步骤仅由一个 goroutine 进行时,一切都很顺利。

让我们讨论当2个 goroutine 同时运行此代码会发生什么。下图描绘了两个 goroutine 同时访问代码行时可能发生的情况。

cs5

图中,第一步协程1当前x值是0,计算x+1,然后系统切换上下文到协程2,第二步,协程2当前x值是0,计算x+1,这时系统又切换上下文到协程1,进行分配x值,然后又切换上下文到协程2,进行分配x值,最后,x的值还是1.

让我们再看看可能发生的不同情况:

cs-6
在上面的场景中,协程1开始执行并完成3个步骤,这时x值是1,然后开始执行协程2,现在x的值已经是1了,在协程2执行完成,x的值就是2了。

因此,在这两种情况下,你可以看到 x 的最终值是1或2取决于上下文切换的方式。这种类型的不良情况,其中程序的输出取决于 goroutine 的执行顺序,称为竞争条件

为了避免竞争条件,可以通过使用 Mutex 实现。

Mutex 互斥

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

sync 包中提供了 Mutex。Mutex 定义了两个方法,即 LockUnlock,在 LockUnlock 之间将仅由一个 goroutine 被执行,从而避免了竞争条件。

1
2
3
mutex.Lock()
x = x + 1
mutex.Unlock()

在上面的代码中,x=x+1将在任何时间点仅由一个 goroutine 执行,从而防止竞争条件。

如果一个 goroutine 已经 Lock,如果一个新的 goroutine 试图 Lock,新的 goroutine 将会阻塞,直到 Mutex Unlock.

play: https://play.golang.org/p/VX9dwGhR62

使用频道解决竞争条件

我们也可以使用通道解决竞争条件。让我们看看如何实现的。

1
2
3
ch <- true
x = x + 1
<- ch

play: https://play.golang.org/p/M1fPEK9lYz

Mutex vs Channel

我们使用互斥锁和通道解决了竞争条件问题。那么我们如何决定何时使用呢?答案在于你要解决的问题。如果你解决的问题更合适互斥锁,那么请继续使用互斥锁。如果需要,请不要犹豫使用互斥锁。如果问题更适合通道,那么使用它:)(没有银弹)

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

一般情况下,当 goroutine 需要互相通信时使用通道,当只有一个 goroutine 应该访问代码的临界区时使用互斥。

在我们上面的问题情况下,我宁愿使用互斥锁,因为这个问题不需要 goroutine 之间任何通信。因此互斥锁是一种自然的选择。

我的建议是根据问题选择工具,不要试图让问题适应工具。