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 接口。


640.png


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


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


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


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


640.png


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


640.png


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


示例代码:


640.png

06

总结


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


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





目录
相关文章
|
3天前
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
52 22
零值在go语言和初始化数据
|
4天前
|
安全 算法 程序员
在go语言中使用泛型和反射
【7月更文挑战第8天】本文介绍go支持泛型后,提升了代码复用,如操作切片、映射、通道的函数,以及自定义数据结构。 泛型适用于通用数据结构和函数,减少接口使用和类型断言。
62 1
在go语言中使用泛型和反射
|
7天前
|
缓存 编译器 Shell
回顾go语言基础中一些特别的概念
【7月更文挑战第6天】本文介绍Go语言基础涵盖包声明、导入、函数、变量、语句和表达式以及注释。零值可用类型如切片、互斥锁和缓冲,支持预分配容量以优化性能。
39 2
回顾go语言基础中一些特别的概念
|
11天前
|
存储 Go API
一个go语言编码的例子
【7月更文挑战第2天】本文介绍Go语言使用Unicode字符集和UTF-8编码。Go中,`unicode/utf8`包处理编码转换,如`EncodeRune`和`DecodeRune`。`golang.org/x/text`库支持更多编码转换,如GBK到UTF-8。编码规则覆盖7位至21位的不同长度码点。
110 1
一个go语言编码的例子
|
3天前
|
JSON Java Go
Go 语言性能优化技巧
在Go语言中优化性能涉及数字字符串转换(如用`strconv.Itoa()`代替`fmt.Sprintf()`)、避免不必要的字符串到字节切片转换、预分配切片容量、使用`strings.Builder`拼接、有效利用并发(`goroutine`和`sync.WaitGroup`)、减少内存分配、对象重用(`sync.Pool`)、无锁编程、I/O缓冲、正则预编译和选择高效的序列化方法。这些策略能显著提升代码执行效率和系统资源利用率。
39 13
|
3天前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
41 8
|
5天前
|
前端开发 JavaScript Go
|
3天前
|
存储 Go
go语言中fmt格式化包和内置函数汇总
【7月更文挑战第10天】本文介绍fmt包和`Errorf`用于创建格式化的错误消息。`fmt`包还涉及一些接口,如`Formatter`、`GoStringer`、`ScanState`、`Scanner`和`Stringer`,支持自定义格式化和输入/输出处理。
16 1
|
3天前
|
Go
go语言中格式化输出的占位符
【7月更文挑战第10天】`fmt` 包在 Go 语言中用于格式化输出,包括不同类型的占位符:%v(默认格式)、%+v(带字段名的结构体)、%#v(Go语法表示)、%T(类型表示)、%%(百分号)。布尔值用%t,整数有%b、%c、%d、%o、%q、%x、%X和%U。浮点数和复数用%b、%e、%E、%f、%g、%G。字符串和字节切片用%s、%q、%x、%X。指针用%p。占位符可配合+、-、#、空格和0进行调整。宽度和精度控制输出格式,例如 %.4g 控制小数精度。Go 没有 `%u`,但无符号整数默认打印为正数。运算符包括逻辑、比较、加减、乘除、移位、按位和按位异或等。
15 1
|
4天前
|
存储 Go 索引
在go语言中自定义泛型的变长参数
【7月更文挑战第8天】在Go语言中,由于官方1.18以前的版本不支持泛型,可以通过空接口和反射模拟泛型。泛型适用于通用数据结构和函数,虽牺牲了一些性能,但提高了代码复用和类型安全性。
41 1