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月前
|
JavaScript 前端开发 Java
通义灵码 Rules 库合集来了,覆盖Java、TypeScript、Python、Go、JavaScript 等
通义灵码新上的外挂 Project Rules 获得了开发者的一致好评:最小成本适配我的开发风格、相当把团队经验沉淀下来,是个很好功能……
921 103
|
5月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
5月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
24天前
|
JSON 编解码 API
Go语言网络编程:使用 net/http 构建 RESTful API
本章介绍如何使用 Go 语言的 `net/http` 标准库构建 RESTful API。内容涵盖 RESTful API 的基本概念及规范,包括 GET、POST、PUT 和 DELETE 方法的实现。通过定义用户数据结构和模拟数据库,逐步实现获取用户列表、创建用户、更新用户、删除用户的 HTTP 路由处理函数。同时提供辅助函数用于路径参数解析,并展示如何设置路由器启动服务。最后通过 curl 或 Postman 测试接口功能。章节总结了路由分发、JSON 编解码、方法区分、并发安全管理和路径参数解析等关键点,为更复杂需求推荐第三方框架如 Gin、Echo 和 Chi。
|
2月前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
64 10
|
4月前
|
Go 开发者
go-carbon v2.6.0 重大版本更新,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持
91 3
|
Go JavaScript
|
22天前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
23天前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
5月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
127 3
Go 语言入门指南:切片