Go语言并发编程(3):sync包介绍和使用(上)-Mutex,RWMutex,WaitGroup,sync.Map
一、sync 包简介
在并发编程中,为了解决竞争条件问题,Go 语言提供了 sync 标准包,它提供了基本的同步原语,例如互斥锁、读写锁等。
sync 包使用建议:
除了 Once 和 WaitGroup 类型之外,大多数类型旨在供低级库程序使用。更高级别的同步最好用 channel 通道和通信来完成。
sync 包中类型:
- sync.Mutex 互斥锁
- sync.RWMutex 读写锁
- sync.WaitGroup 等待组(等待一组 goroutine 完成)
- sync.Map 并发 Map
- sync.Once 执行一次
- sync.Pool 对象池
- sync.Cond 条件变量
此外,sync 下还有一个包 atomic, 它提供了对数据的原子操作。
另外,Go 的扩展包也提供了信号量这种同步原语:
- x/sync/semaphore
二、sync.Mutex 互斥锁
sync.Mutex 是一个互斥锁,它的作用就是保护临界区,确保同一时间只有一个 Go 协程进入临界区。
什么是临界区?为什么有临界区?
在并发编程中,有一部分程序被并发访问,这个访问可能是多个协程/线程修改这部分程序数据,这样的操作会导致意想不到的结果,为了不让操作导致意外结果,怎么办?就需要把这部分程序保护起来,一次只允许一个协程/线程访问这部分区域。需要被保护的这部分程序区域就叫临界区。
防止多个协程/线程同时进入临界区,修改程序数据。
互斥锁就是一种可以保护临界区资源方式。
互斥锁其实是一种最特殊的信号量,这个"量"只有 0 和 1,所以也叫互斥量。互斥量的值为 0 和 1,用来表示加锁和解锁。互斥锁是一种独占锁,即同一时间只能有一个协程持有锁,其他协程必须等待。
互斥锁使得同一时刻只有一个协程执行某段程序,其他协程等待该协程执行完在抢锁后执行。

如上图所示:g1 用互斥锁保护临界区,g2 在中间尝试获取锁失败,g1 离开临界区释放锁,g2 获取到锁然后进行相应操作,操作完后释放锁离开临界区。
第一次使用后不得复制 Mutex。
互斥锁使用:
- 互斥锁有两个方法
Lock()加锁和Unlock()解锁,他们是成对出现。当一个协程对资源上锁后,其他协程只能等待该协程解锁之后,才能再次上锁。 - 它还有一个
TryLock(),go1.18 之后添加的。- 当一个 goroutine 调用此方法试图获取锁时,如果这把锁没有被其他 goroutine 持有,那么这个 goroutine 获取锁并返回 true;
- 如果这把锁已经被其它 goroutine 持有,或正准备给某个唤醒的 gorouine,那么请求锁的 goroutine 直接返回 false,不会阻塞在方法调用上。
Lock()
代码段(临界区)
Unlock()
为了防止上锁后忘记释放锁,实际使用中用 defer 来释放锁。
例子:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
var lock sync.Mutex
for i := 0; i < 100; i++ { // 并发 100 个goroutine
go func(id int) {
lock.Lock()
defer lock.Unlock()
a += 1
fmt.Printf("goroutine %d, a=%d\n", id, a)
}(i)
}
time.Sleep(time.Second) //等待1秒, 确保所有的协程执行完
}
三、sync.RWMutex 读写锁
sync.RWMutex 读写锁,对数据操进加锁进一步细分,针对读操作和写操作分别进行加锁和解锁。
在读写锁下,读操作和读操作之间不互斥,多个写操作是互斥,读操作和写操作也是互斥。
- 当一个 goroutine 获取读锁之后,其它的 goroutine 此时想获取读锁,那么可以继续获取锁,不用等待解锁;此时想获取写锁,就会阻塞等待直到读解锁;
- 当一个 goroutine 获取写锁之后,其它的 goroutine 无论是获取读锁还是写锁,都会阻塞等待。
读写锁的好处:
多个读之间不互斥,读锁就可以降低对数据读取加互斥锁的性能损耗。而不像互斥锁那样对所有的数据操作,不管是读还是写,同等对待,都加一把大锁处理。
在读多写少的场景下,更适合用读写锁。
RWMutex 读写锁的方法:
- Mutex 的加锁和解锁:Lock() 和 Unlock()
- 只读加锁和加锁:RLock() 和 RUnlock()
- RLock() 加读锁时如果存在写锁,则不能加锁;当只有读锁或无锁时,可以加读锁,且读锁可以加载多个。
- RUnlock() 解读锁。没有读锁情况下调用 RUlock() 会导致 panic。
释放锁用 defer 来释放锁
// 使用 RWMutex 的伪码,当然正式代码不会这样写,会用 defer 释放锁
mutex := sync.RWMutex{}
mutex.Lock()
// 操作的资源
mutex.Unlock()
mutex.RLock()
// 读的资源
mutex.RUlock()
例子:
package main
import (
"fmt"
"sync"
"time"
)
var sum = 0
var rwMutex sync.RWMutex
func main() {
// 并发写
for i := 1; i <= 50; i++ {
go writeSum()
}
// 并发读
for i := 1; i <= 20; i++ {
go fmt.Println("readSum: ", readSum())
}
time.Sleep(time.Second * 2) // 防止主程序退出,子协程还没运行完
fmt.Println("end sum: ", sum)
}
func writeSum() {
rwMutex.Lock() // 读写锁
defer rwMutex.Unlock() // 释放锁
sum += 1
}
func readSum() int {
rwMutex.RLock() // 读写锁加读锁
defer rwMutex.RUnlock() // 释放读锁
return sum
}
四、sync.WaitGroup 等待组
sync.WaitGroup,等待一组或多个 goroutine 执行完成。
WaitGroup 内部有一个安全的计数器,它调用 Add(n int) 方法把计数器 +n;使用 Done() 方法,将计数器减 1,Done() 的底层是调用 Add(-1);调用 Wait() 方法等待所有的 goroutine 执行完,即计数器为 0,Wait() 就返回。
WaitGroup 详细原理,可以看我前面的文章:sync.WaitGroup源码分析。
- WaitGroup 里的方法:
- Add(n),设置要等待的子 goroutine 数量,n 表示要等待数量
- Done(),子 goroutine 执行完后,计数器减一
- Wait(),阻塞等待所有子 goroutine 执行完
例子:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("子 goroutine1")
}()
go func() {
defer wg.Done()
fmt.Println("子 goroutine2")
}()
wg.Wait() // 等待所有的子goroutine结束
fmt.Println("程序运行结束")
}
五、sync.Map 并发Map
在 Go 语言中,内置数据结构 map 并不是并发安全的,所以官方就出了一个 sync.Map。
希望了解 sync.Map 的原理,可以看这篇文章:深入理解Go语言(05):sync.map原理分析。
sync.Map 里的常用方法:
go v1.20.1
- Store(key, value any),设置键的值
- Load(key any) (value any, ok bool),获取值
- Delete(key any),根据 key 删除值
- LoadAndDelete(key any) (value any, loaded bool),根据 key 删除值,返回以前的值如果还存在
- LoadOrStore(key, value any)(actual any, loaded bool),先根据 key 查找 value,如果找到则返回原来的值,loaded 为 true;如果没有找到 key 对应的 value 值,则存在 key,value 值并将存储值返回,loaded 为 false
- Range(f func(key, value any) bool),遍历 sync.Map 的元素
更多方法请查看:https://pkg.go.dev/sync#Map
例子:
package main
import (
"fmt"
"sync"
)
func main() {
var syncmap sync.Map
syncmap.Store("li", 12)
syncmap.Store("han", "lu")
syncmap.Store("mei", 34)
fmt.Println(syncmap.Load("han"))
// key 不存在
val, ok := syncmap.LoadOrStore("lei", "lei")
fmt.Println(val, ok)
// key 存在
val, ok = syncmap.LoadOrStore("han", "cunzai")
fmt.Println(val, ok)
syncmap.Delete("mei")
syncmap.Range(func(k, v any) bool {
fmt.Println("k-v: ", k, v)
return true
})
}
参考
- https://pkg.go.dev/sync sync
- https://www.cnblogs.com/jiujuan/p/13365901.html sync.Map 原理
Go语言并发编程(3):sync包介绍和使用(上)-Mutex,RWMutex,WaitGroup,sync.Map的更多相关文章
- 并发编程--Concurrent-工具类介绍
并发编程--Concurrent-工具类介绍 并发编程--Concurrent-工具类介绍 CountDownLatch CylicBarrier Semaphore Condition 对象监视器下 ...
- Go语言 并发编程
Go语言 并发编程 作者:Eric 微信:loveoracle11g 1.创建goroutine // 并行 是两个队列同时使用两台咖啡机 // 并发 是两个队列交替使用一台咖啡机 package m ...
- 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
2007年诞生的Go语言,凭借其近C的执行性能和近解析型语言的开发效率,以及近乎完美的编译速度,席卷全球.Go语言相关书籍也如雨后春笋般涌现,前不久,一本名为<Go语言并发之道>的书籍被翻 ...
- Go语言并发编程总结
转自:http://blog.csdn.net/yue7603835/article/details/44309409 Golang :不要通过共享内存来通信,而应该通过通信来共享内存.这句风靡在Go ...
- go语言并发编程
引言 说到go语言最厉害的是什么就不得不提到并发,并发是什么?,与并发相关的并行又是什么? 并发:同一时间段内执行多个任务 并行:同一时刻执行多个任务 进程.线程与协程 进程: 进程是具有一定独立功能 ...
- Go语言并发编程示例 分享(含有源代码)
GO语言并发示例分享: ppt http://files.cnblogs.com/files/yuhan-TB/GO%E8%AF%AD%E8%A8%80.pptx 代码, 实际就是<<Go ...
- Java 面试宝典!并发编程 71 道题及答案全送上!
金九银十跳槽季已经开始,作为 Java 开发者你开始刷面试题了吗?别急,我整理了71道并发相关的面试题,看这一文就够了! 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程( ...
- JUC并发编程与高性能内存队列disruptor实战-上
JUC并发实战 Synchonized与Lock 区别 Synchronized是Java的关键字,由JVM层面实现的,Lock是一个接口,有实现类,由JDK实现. Synchronized无法获取锁 ...
- Java并发编程-volatile可见性的介绍
要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? ...
- 【并发编程】Executor架构介绍
要点总结 Executor表示的任务类型 主要有3种: Runnable: 无返回值,无异常抛出: Callable:有返回值,可以异常抛出: Future任务: 表示异步计算,可取消: 通过newT ...
随机推荐
- [转帖]Linux下进程管理知识(详细)总结
一.简介 本文主要详细介绍进程相关的命令的使用.进程管理及调度策略的知识. 二.常用的命令解析 1.ps命令 命令选项 解析 -a 显示一个终端所有的进程 -u 显示进程的归属用户和内存占用情况 -x ...
- [转帖]银河麒麟高级服务器操作系统V10SP1安装Docker管理工具(Portainer+DockerUI)
文章目录 一.系统环境配置 二.安装Docker 三.安装Docker管理工具 Docker管理工具之Portainer Portainer简介 Portainer安装 Portainer访问测试 D ...
- [转帖]QPS、TPS、RT、并发数、吞吐量理解和性能优化深入思考
https://baijiahao.baidu.com/s?id=1675704570461446033&wfr=spider&for=pc 吞吐量 在了解qps.tps.rt.并发数 ...
- ElasticSearch降本增效常见的方法 | 京东云技术团队
Elasticsearch在db_ranking 的排名不断上升,其在存储领域已经蔚然成风且占有非常重要的地位. 随着Elasticsearch越来越受欢迎,企业花费在ES建设上的成本自然也不少.那如 ...
- el-popover 点击取消按钮,弹窗仍然无法关闭
<el-popover placement="bottom" width="200" :ref="aa" :visible.sync= ...
- vue mixin混入 全局混入 局部混入
<div id="app"> --{{nameName}} </div> // 全局混入 不需要注册 var m1 = Vue.mixin({ data() ...
- 【四】多智能体强化学习(MARL)近年研究概览 {Learning cooperation(协作学习)、Agents modeling agents(智能体建模)}
相关文章: [一]最新多智能体强化学习方法[总结] [二]最新多智能体强化学习文章如何查阅{顶会:AAAI. ICML } [三]多智能体强化学习(MARL)近年研究概览 {Analysis of e ...
- 【五】gym搭建自己的环境之寻宝游戏,详细定义自己myenv.py文件以及算法实现
相关文章: 相关文章: [一]gym环境安装以及安装遇到的错误解决 [二]gym初次入门一学就会-简明教程 [三]gym简单画图 [四]gym搭建自己的环境,全网最详细版本,3分钟你就学会了! [五] ...
- 18.3 NPCAP自定义数据包过滤
NPCAP 库是一种用于在Windows平台上进行网络数据包捕获和分析的库.它是WinPcap库的一个分支,由Nmap开发团队开发,并在Nmap软件中使用.与WinPcap一样,NPCAP库提供了一些 ...
- Linux的信号管理 [补档-2023-07-30]
信号 11-1简介: 信号只是表示某个信号,不可以携带大量信息,信号需要满足特点的条件才会产生.是一种特别的通信手 段. 11-2 信号机制: 假设有两个进程A,B,现在进程A给进程B发送信号 ...