25

Go 语言使用标准库 sync 包的 mutex 互斥锁解决数据静态

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzA4Mjc1NTMyOQ%3D%3D&%3Bmid=2247484031&%3Bidx=1&%3Bsn=7b6728f4f781a6fcdcf1986404b1ad59
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MBJz6zJ.png!mobile

01

介绍

在 Go 语言中,Go 标准库 sync 包中有一个单独的 Mutex 类型,它支持互斥锁模式。Mutex 类型的 Lock 方法用于获取 token,Unlock 方法用于释放 token。

定义的 Mutex 类型的变量称为互斥量,用来保护共享变量(临界区)。被互斥量保护的变量声明应该紧接在互斥量的声明之后。

为了防止未执行 Unlock 方法,通常在 Lock 方法后,使用 defer 语句调用 Unlock 方法。

02

基本使用

在 Go 语言中,Go 标准库 sync 包提供了一系列锁相关的同步原语,sync 包还定义了一个 Locker 接口,并且 Mutex 就实现了 Locker 接口。

6B3I3ya.png!mobile

通过代码,可以看出 Go 标准库 sync 包定义的 Locker 接口非常简单,只有一个锁请求 Lock 方法和一个锁释放 Unlock 方法。

在 Go 语言项目开发中,Locker 接口使用的并不多,我们一般会直接使用具体的同步原语,比如 Mutex。

下面通过代码示例,演示 Mutex 的基本使用。

在演示 Mutex 之前,我们先列举一个反例,了解 Go 语言的读者应该知道,在 Go 语言中实现并发非常简单,只需要在函数前面加上一个 go 关键字,我们通过一个并发计数的示例,先来演示一下在不使用 Mutex 互斥锁时,go 并发计数的结果是什么。

6n6Rvem.png!mobile

示例代码中,我们先不使用 Mutex 互斥锁,定义一个 count 变量,通过 10 个 goroutine (协程)并发给 count 变量累加 100000 次,通过运行 go run main.go,发现结果并不是预想的结果 1000000(100万)。

原因其实很简单,因为 count++ 并不是一个原子性操作,它至少包含以下 3 个步骤,1 是读取 count 变量的值,2 是将 count 变量的值加 1,3 是将加 1 的值赋给 count 变量。这 3 个步骤因为不是原子性操作,所以就会出现并发问题,比如 goroutine1 和 goroutine2 同时读取到 count 变量的值为 10,这两个 goroutine 都按照自己读取到的 count 变量的值加 1,count 变量的值变为 11,但是 count 变量的值实际应该是 12,这就是并发访问共享数据的常见错误,也就是我们常说的数据竞态。

不用担心,可以使用 Mutex 互斥锁解决数据竞态问题。我们已经知道,并发计数的共享变量是 count 变量,也就是说 count++ 变量是临界区,只要我们在临界区前后加上锁获取和锁释放,就可以解决数据竞态问题。

Z7n6Jf3.png!mobile

通过代码可以看出,我们只对代码进行了简单修改,声明一个 Mutex 类型的变量 mu,在临界区 count++ 前后加上了锁获取 mu.Lock() 和锁释放mu.Unlock(),运行修改后的代码,go run main.go,发现并发计数的结果变成了我们预想的结果 1000000(100万),解决了并发计数的数据竞态问题。

03

实现原理

如果读者阅读过 Go 标准库 sync 包中的 Mutex 源码,一定会体会到 Go 语言作者精湛的软件设计思想。

在 Go1.9 版本开始,Go 作者给 Mutex 增加了「饥饿模式」,在正常模式中,等待的 goroutine 存放在一个先进先出的队列中,但是,新 goroutine 可以和等待队列中的队头 goroutine 竞争,并且有固定数量的最大竞争次数,一次没有竞争过,可以再次竞争,直到达到固定的最大竞争次数。等待队列中的队头 goroutine 如果没有竞争过新 goroutine,就会重新插入等待队列中的队头,如果等待队列中的队头 goroutine 没有竞争过多个新 goroutine(等待时间超过 1ms),正常模式就会转换为饥饿模式。

在饥饿模式中,新 goroutine 不再和等待队列中的队头 goroutine 竞争,新 goroutine 主动让出,并插入到等待队列的队尾。

如果持有锁的 goroutine 发现等待队列中已经没有其他等待的 goroutine 或者持有锁的 goroutine 本次等待时间小于 1ms,饥饿模式就会转换为正常模式。

04

踩坑

在 Go 语言项目开发中,难免由于开发者的疏忽,忘掉 Lock 或 Unlock,导致锁不成对出现。

在忘掉 Unlock 的情况下,锁获取后永远不会得到释放,其他 goroutine 永远处于阻塞状态,永远获取不到锁。

在忘掉 Lock 的情况下,直接 Unlock 一个未加锁的 Mutex,会导致程序 panic。

05

拓展使用

使用 Mutex 实现线程(goroutine)安全队列

在 Go 语言中,我们可以通过 Slice 实现一个队列,但是 Slice 实现的队列不是线程安全的,入队和出队会发生数据竞态,不用担心,我们可以使用 Mutex 的锁机制,在入队和出队的时候加上锁保护,保证线程安全。

示例代码:

U7Vr6fJ.png!mobile

06

总结

文章开头先是简单介绍了 Go 语言标准库 sync 包的 Mutex 类型,然后通过并发计数的示例代码演示了 Mutex 互斥锁的基本使用。文章还简单介绍了 Mutex 的实现原理和在项目开发中容易踩到的「坑」,最后通过实现一个线程安全的队列,演示了 Mutex 拓展使用的案例。

文章中的代码完整版本,请点击「阅读原文」,在 Github 中阅读。

免费领取资料

扫描下方二维码关注微信公众号「Golang 语言开发栈」,回复「 资料 」关键字,免费获取 Golang 语言学习资料,回复「 微信群 」关键字,申请加入微信群,和我一起学习 Golang。

JvMZNbr.jpg!mobile

推荐阅读:

Go 语言学习之 goroutine 和 channel

Go语言学习之并发


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK