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

微信扫一扫,分享到朋友圈

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

01

介绍

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

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

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

02

基本使用

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

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

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

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

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

示例代码中,我们先不使用 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++ 变量是临界区,只要我们在临界区前后加上锁获取和锁释放,就可以解决数据竞态问题。

通过代码可以看出,我们只对代码进行了简单修改,声明一个 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 的锁机制,在入队和出队的时候加上锁保护,保证线程安全。

示例代码:

06

总结

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

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

免费领取资料

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

推荐阅读:

Go 语言学习之 goroutine 和 channel

Go语言学习之并发

微信扫一扫,分享到朋友圈

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

时序数据库 | Monarch: 谷歌的全球级内存时序数据库

上一篇

PyTorch 实现 Skip-gram

下一篇

你也可能喜欢

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

长按储存图像,分享给朋友