APP下载

GCTT 出品Go 系列教程——25. Mutex

消息来源:baojiabao.com 作者: 发布时间:2024-05-20

报价宝综合消息GCTT 出品Go 系列教程——25. Mutex

Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!

Go 系列教程是非常棒的一套初学者教程,入门就它了。

这是 Golang 系列教程中的第 25 篇。在本章教程中,我们将讨论 Go 语言中的Mutex。

临界区

在学习 Mutex 之前,我们需要理解并发程式设计中临界区(Critical Section)的概念。当程式并发地执行时,多个 Go 协程不应该同时访问那些修改共享资源的程式码。这些修改共享资源的程式码称为临界区。例如,假设我们有一段程式码,将一个变数 x 自增 1。

x = x + 1

如果只有一个 Go 协程访问上面的程式码段,那都没有任何问题。

但当有多个协程并发执行时,程式码却会出错,让我们看看究竟是为什么吧。简单起见,假设在一行程式码的前面,我们已经运行了两个 Go 协程。

在上一行程式码的内部,系统执行程式时分为如下几个步骤(这里其实还有很多包括暂存器的技术细节,以及加法的工作原理等,但对于我们的系列教程,只需认为只有三个步骤就好了):

获得 x 的当前值计算 x + 1将步骤 2 计算得到的值赋值给 x如果只有一个协程执行上面的三个步骤,不会有问题。

我们讨论一下当有两个并发的协程执行该程式码时,会发生什么。下图描述了当两个协程并发地访问程式码行 x = x + 1 时,可能出现的一种情况。

我们假设 x 的初始值为 0。而协程 1 获取 x 的初始值,并计算 x + 1。而在协程 1 将计算值赋值给 x 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 x 的初始值(依然为 0),并计算 x + 1。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 x,因此 x 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 x,因此在所有协程执行完毕之后,x 都等于 1。

现在我们考虑另外一种可能发生的情况。

在上面的情形里,协程 1 开始执行,完成了三个步骤后结束,因此 x 的值等于 1。接着,开始执行协程 2。目前 x 的值等于 1。而当协程 2 执行完毕时,x 的值等于 2。

所以,从这两个例子你可以发现,根据上下文切换的不同情形,x 的最终值是 1 或者 2。这种不太理想的情况称为竞态条件(Race Condition),其程式的输出是由协程的执行顺序决定的。

在上例中,如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的

Mutex

Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区执行,以防止出现竞态条件。

Mutex 可以在 sync 包内找到。Mutex 定义了两个方法:LockUnlock。所有在 Lock 和 Unlock 之间的程式码,都只能由一个 Go 协程执行,于是就可以避免竞态条件。

在上面的程式码中,x = x + 1 只能由一个 Go 协程执行,因此避免了竞态条件。

如果有一个 Go 协程已经持有了锁(Lock),当其他协程试图获得该锁时,这些协程会被阻塞,直到 Mutex 解除锁定为止。

含有竞态条件的程式

在本节里,我们会编写一个含有竞态条件的程式,而在接下来一节,我们再修复竞态条件的问题。

在上述程式里,第 7 行的 increment 函式把 x 的值加 1,并呼叫 WaitGroup 的 Done(),通知该函式已结束。

在上述程式的第 15 行,我们生成了 1000 个 increment 协程。每个 Go 协程并发地执行,由于第 8 行试图增加 x 的值,因此多个并发的协程试图访问 x 的值,这时就会发生竞态条件。

由于 playground 具有确定性,竞态条件不会在 playground 发生,请在你的本地执行该程式。请在你的本地机器上多执行几次,可以发现由于竞态条件,每一次输出都不同。我其中遇到的几次输出有 final value of x 941、final value of x 928、final value of x 922 等。

使用 Mutex

在前面的程式里,我们建立了 1000 个 Go 协程。如果每个协程对 x 加 1,最终 x 期望的值应该是 1000。在本节,我们会在程式里使用 Mutex,修复竞态条件的问题。

Mutex 是一个结构体型别,我们在第 15 行建立了 Mutex 型别的变数 m,其值为零值。在上述程式里,我们修改了 increment 函式,将增加 x 的程式码(x = x + 1)放置在 m.Lock() 和 m.Unlock()之间。现在这段程式码不存在竞态条件了,因为任何时刻都只允许一个协程执行这段程式码。

于是如果执行该程式,会输出:

final value of x 1000

在第 18 行,传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。

使用通道处理竞态条件

我们还能用通道来处理竞态条件。看看是怎么做的。

在上述程式中,我们建立了容量为 1 的缓冲通道,并在第 18 行将它传入 increment 协程。该缓冲通道用于保证只有一个协程访问增加 x 的临界区。具体的实现方法是在 x 增加之前(第 8 行),传入 true 给缓冲通道。由于缓冲通道的容量为 1,所以任何其他协程试图写入该通道时,都会发生阻塞,直到 x 增加后,通道的值才会被读取(第 10 行)。实际上这就保证了只允许一个协程访问临界区。

该程式也输出:

final value of x 1000

Mutex vs 通道

通过使用 Mutex 和通道,我们已经解决了竞态条件的问题。那么我们该选择使用哪一个?答案取决于你想要解决的问题。如果你想要解决的问题更适用于 Mutex,那么就用 Mutex。如果需要使用 Mutex,无须犹豫。而如果该问题更适用于通道,那就使用通道。:)

由于通道是 Go 语言很酷的特性,大多数 Go 新手处理每个并发问题时,使用的都是通道。这是不对的。Go 给了你选择 Mutex 和通道的余地,选择其中之一都可以是正确的。

总体说来,当 Go 协程需要与其他协程通讯时,可以使用通道。而当只允许一个协程访问临界区时,可以使用 Mutex。

就我们上面解决的问题而言,我更倾向于使用 Mutex,因为该问题并不需要协程间的通讯。所以 Mutex 是很自然的选择。

我的建议是去选择针对问题的工具,而别让问题去将就工具。:)

本教程到此结束。祝你愉快。

上一教程 -“GCTT 出品”Go 系列教程——24. Select

下一教程 - 结构体取代类

历史文章:

“GCTT 出品”Go 系列教程——1. 介绍与安装

“GCTT 出品”Go 系列教程——2. Hello World

“GCTT 出品”Go 系列教程——3. 变数

“GCTT 出品”Go 系列教程——4. 型别

“GCTT 出品”Go 系列教程——5. 常量

“GCTT 出品”Go 系列教程——6. 函式(Function)

“GCTT 出品”Go 系列教程——7. 包

Go 系列教程——8. if-else 语句

“GCTT 出品”Go 系列教程——9. 循环

“GCTT 出品”Go 系列教程——10. switch 语句

“GCTT 出品”Go 系列教程——11. 阵列和切片

“GCTT 出品”Go 系列教程——12. 可变引数函式

“GCTT 出品”Go 系列教程——13. Maps

“GCTT 出品”Go 系列教程——14. 字串

“GCTT 出品”Go 系列教程——15. 指标

“GCTT 出品”Go 系列教程——16. 结构体,这一篇就够

“GCTT 出品”Go 系列教程——17. 超全的方法教程

“GCTT 出品”Go 系列教程——18. 界面(一)

“GCTT 出品”Go 系列教程——19. 界面(二)

“GCTT 出品”Go 系列教程——20. 并发入门

“GCTT 出品”Go 系列教程——21. Go 协程

“GCTT 出品”Go 系列教程——22. 通道(channel)

“GCTT 出品”Go 系列教程——23. 缓冲通道和工作池

“GCTT 出品”Go 系列教程——24. Select

2019-07-18 12:00:00

相关文章