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 中阅读。





目录
相关文章
|
8月前
|
人工智能 安全 Shell
Go并发编程避坑指南:从数据竞争到同步原语的解决方案
在高并发场景下,如钱包转账,数据一致性至关重要。本文通过实例演示了 Go 中如何利用 `sync.Mutex` 和 `sync.RWMutex` 解决数据竞争问题,帮助开发者掌握并发编程中的关键技能。
Go并发编程避坑指南:从数据竞争到同步原语的解决方案
|
11月前
|
Go 开发者
Go语言包的组织与导入 -《Go语言实战指南》
本章详细介绍了Go语言中的包(Package)概念及其使用方法。包是实现代码模块化、复用性和可维护性的核心单位,内容涵盖包的基本定义、命名规则、组织结构以及导入方式。通过示例说明了如何创建和调用包,并深入讲解了`go.mod`文件对包路径的管理。此外,还提供了多种导入技巧,如别名导入、匿名导入等,帮助开发者优化代码结构与可读性。最后以表格形式总结了关键点,便于快速回顾和应用。
455 61
|
7月前
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
373 4
|
10月前
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
585 63
|
8月前
|
存储 监控 算法
企业电脑监控系统中基于 Go 语言的跳表结构设备数据索引算法研究
本文介绍基于Go语言的跳表算法在企业电脑监控系统中的应用,通过多层索引结构将数据查询、插入、删除操作优化至O(log n),显著提升海量设备数据管理效率,解决传统链表查询延迟问题,实现高效设备状态定位与异常筛选。
209 3
|
11月前
|
Go
Go语言同步原语与数据竞争:Mutex 与 RWMutex
在Go语言并发编程中,数据竞争是多个goroutine同时读写共享变量且未加控制导致的问题,可能引发程序崩溃或非确定性错误。为解决此问题,Go提供了`sync.Mutex`和`sync.RWMutex`两种同步机制。`Mutex`用于保护临界区,确保同一时间只有一个goroutine访问;`RWMutex`支持多读单写的细粒度控制,适合读多写少场景。使用时需避免死锁,并借助`-race`工具检测潜在的数据竞争,从而提升程序稳定性和性能。
312 51
|
9月前
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
693 0
|
11月前
|
编译器 测试技术 Go
Go语言同步原语与数据竞争:数据竞争的检测工具
本文介绍了 Go 语言中数据竞争(Data Race)的概念及其检测方法。数据竞争发生在多个 Goroutine 无同步访问共享变量且至少一个为写操作时,可能导致程序行为不稳定或偶发崩溃。Go 提供了内置的竞态检测器(Race Detector),通过 `-race` 参数可轻松检测潜在问题。文章还展示了如何使用锁或原子操作修复数据竞争,并总结了在开发和 CI 流程中启用 `-race` 的最佳实践,以提升程序稳定性和可靠性。
|
11月前
|
Go
Go语言同步原语与数据竞争:WaitGroup
本文介绍了 Go 语言中 `sync.WaitGroup` 的使用方法和注意事项。作为同步原语,它通过计数器机制帮助等待多个 goroutine 完成任务。核心方法包括 `Add()`(设置等待数量)、`Done()`(减少计数)和 `Wait()`(阻塞直到计数归零)。文章详细讲解了其基本原理、典型用法(如等待 10 个 goroutine 执行完毕),并提供了代码示例。同时指出常见错误,例如 `Add()` 必须在 goroutine 启动前调用,以及 WaitGroup 不可重复使用。最后总结了适用场景和使用要点,强调避免竞态条件与变量捕获陷阱。
|
11月前
|
安全 Go 调度
Go同步原语与数据竞争:原子操作(atomic)
本文介绍了Go语言中`sync/atomic`包的使用,帮助避免多goroutine并发操作时的数据竞争问题。原子操作是一种不可中断的操作,确保变量读写的安全性。文章详细说明了常用函数如`Load`、`Store`、`Add`和`CompareAndSwap`的功能与应用场景,并通过并发计数器示例展示了其实现方式。此外,对比了原子操作与锁的优缺点,强调原子操作适用于简单变量的高效同步,而不适合复杂数据结构。最后提醒开发者注意使用场景限制,合理选择同步工具以优化性能。

热门文章

最新文章