Go 并发编程 - 并发安全(二)
什么是并发安全
并发情况下,多个线程或协程会同时操作同一个资源,例如变量、数据结构、文件等。如果不保证并发安全,就可能导致数据竞争、脏读、脏写、死锁、活锁、饥饿等一系列并发问题,产生重大的安全隐患,比如12306抢到同一张火车票、多个用户抢到只剩一件库存的商品。而并发安全就是为了避免这些问题。Golang 中有一些原则和工具来保证并发安全,例如:
- 遵循“通过通信来共享内存,而不是通过共享内存通信”的理念,尽量使用 channel 来传递数据,而不是使用共享变量。
- 如果必须使用共享变量,那么要使用合理的锁来避免数据竞争。
- 如果使用锁,要注意锁的粒度和范围,尽量减少锁的持有时间和影响范围,避免死锁和活锁。
关于更为详细的并发安全性:可以参考:理解Golang 赋值的并发安全性。
资源竞争
所有资源竞争就是多个 goroutine 访问某个共享的资源,我们来看一个资源竞争的例子:
var wg sync.WaitGroup
func add(count *int) {
defer wg.Done()
for i := 0; i < 10000; i++ {
*count = *count + 1
}
}
func main() {
count := 0
wg.Add(3)
for i := 0; i < 3; i++ {
go add(&count)
}
wg.Wait()
fmt.Println(count)
}
该程序的每一次执行结果都不同, 就是因为协程之间出现了资源竞争,在读取更新 count 这个过程中,被其他协程横插了一脚,改变了 count 的值,没有保证原子性。下面我们通过互斥锁来锁住在读取更新过程的 count 的值,来使 count 的值打印正确。
互斥锁和读写互斥锁
sync 包提供了通过 sync.Mutex 和 sync.RWMutex 来实现互斥锁和读写互斥锁。
sync 互斥锁(sync.Mutex)是一种最简单的锁类型,当一个 goroutine 获得了资源后,其他 goroutine 就只能等待这个 goroutine 释放该资源。互斥锁可以保证对共享资源的原子访问,避免并发冲突。
sync 读写互斥锁(sync.RWMutex)是一种更复杂的锁类型,它允许多个 goroutine 同时获取读锁,但只允许一个 goroutine 获取写锁。读写互斥锁适用于读多写少的场景下,它比互斥锁更高效。
sync.Mutex
sync.Mutex 使用 Lock() 加锁,Unlock() 解锁,如果对未解锁的 Mutex 使用 Lock() 会阻塞当前程序运行,我们来看加入了互斥锁后的程序:
var wg sync.WaitGroup
var l sync.Mutex
func add(count *int) {
defer wg.Done()
l.Lock() // 锁住 count 资源,阻塞程序运行,直到 Unlock
for i := 0; i < 10000; i++ {
*count = *count + 1
}
l.Unlock()
}
func main() {
count := 0
wg.Add(3)
for i := 0; i < 3; i++ {
go add(&count)
}
wg.Wait()
fmt.Println(count)
}
sync.RWMutex
- RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。
- 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取资源,使用
RLock和RUnlock加锁解锁。 - 写锁会阻止其他 goroutine 进来,读写不论,整个锁住的资源由该 goroutine 独占,使用
Lock和Unlock加锁解锁。 - 应该只在频繁读取,少量写入的情况下使用读写互斥锁
var m sync.RWMutex
var i = 0
func main() {
go write()
go write()
go read()
go read()
go read()
time.Sleep(2 * time.Second)
}
func read() {
fmt.Println(i, "我准备获取读锁了")
m.RLock()
fmt.Println(i, "我要开始读数据了,所有写数据的都需要等待1s")
time.Sleep(1 * time.Second)
m.RUnlock()
fmt.Println(i, "我已经释放了读锁,可以继续写数据了")
}
func write() {
fmt.Println(i, "我准备获取写锁了")
m.Lock()
fmt.Println(i, "我要开始写数据了,所有人都需要等待1s")
time.Sleep(1 * time.Second)
i++
m.Unlock()
fmt.Println(i, "我已经释放了写锁,你们可以继续了")
}
// 结果
0 我准备获取读锁了
0 我要开始读数据了,所有写数据的都需要等待1s
0 我准备获取读锁了
0 我要开始读数据了,所有写数据的都需要等待1s
0 我准备获取读锁了
0 我要开始读数据了,所有写数据的都需要等待1s
0 我准备获取写锁了
0 我准备获取写锁了
0 我已经释放了读锁,可以继续写数据了
0 我已经释放了读锁,可以继续写数据了
0 我已经释放了读锁,可以继续写数据了
0 我要开始写数据了,所有人都需要等待1s
1 我已经释放了写锁,你们可以继续了
读写互斥锁有点难以理解,但是只要记住读写互斥永远是互斥的,就理解了大半。为了应对读锁长久占用,导致写锁迟迟不能更新数据,导致并发饥饿问题,所以在 Golang 的读写互斥锁中,写锁比读锁优先级更高。
sync.once
sync.once 是一个极为强大的功能,它可以确保一个函数只能被执行一次。通常做来在并发执行前初始化一次的共享资源。
func main() {
once := &sync.Once{}
for i := 0; i < 10; i++ {
go func(i int) {
once.Do(func() {
fmt.Printf("i的值 %d\n", i)
})
}(i)
}
time.Sleep(1 * time.Second)
}
这段代码始终只会打印一次 i 的值。
原子操作
为了实现变量值的并发情况下安全赋值,除了互斥锁外,Golang 还提供了 atomic 包,他能保证在变量在读写时不受其他 goroutine 影响。atomic 是通过 CPU 指令在硬件层面上实现的,比互斥锁性能更好。当然,互斥锁一般来说是对代码块的并发控制,atomic 是对某个变量的并发控制,二者侧重点不同。另外,atomic 是一个很底层的包,除非在一些非常追求的性能的地方,否则其他地方都不推荐使用。
atomic.Add
add 方法比较容易理解,就是对一个值进行增加操作:
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
使用示例:
var a int32 = 1
atomic.AddInt32(&a, 2)
fmt.Println(a) // 输出3
atomic.AddInt32(&a, -1) // delta 是负值的话会减少该值
fmt.Println(a) // 输出2
atomic.CompareAndSwap
CompareAndSwap用作比较置换值,如果等于,则更新值,返回 true,否则返回 false:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
使用示例:
var (
a int32 = 1
b bool
)
b = atomic.CompareAndSwapInt32(&a, 1, 2)
fmt.Println(a) // 输出2
fmt.Println(b) // 输出true
b = atomic.CompareAndSwapInt32(&a, 1, 3)
fmt.Println(a) // 输出2
fmt.Println(b) // 输出false
atomic.Swap
Swap方法不比较,直接置换值:
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
使用示例:
var (
a int32 = 1
old int32
)
old = atomic.SwapInt32(&a, 2)
fmt.Println(a) // 输出2
fmt.Println(old) // 输出1
atomic.Load
Load 用来读取值:
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
使用示例:
var (
a int32 = 1
value int32
)
value = atomic.LoadInt32(&a)
fmt.Println(value) // 输出1
atomic.Store
Store 用来将一个值存到变量中,Load 不会读取到存到一半的值:
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
使用示例:
var a int32
atomic.StoreInt32(&a, 1)
fmt.Println(a) // 输出1
atomic.Value
Value 实现了对任意值的存储、读取、置换、比较置换:
func (v *Value) Store(val any)
func (v *Value) Load() (val any)
func (v *Value) Swap(new any) (old any)
func (v *Value) CompareAndSwap(old, new any)
使用示例:
var v atomic.Value
v.Store(1)
fmt.Println(v.Load()) // 1
v.Swap(2)
fmt.Println(v.Load()) // 2
b := v.CompareAndSwap(2, 3)
fmt.Println(v.Load()) // 3
fmt.Println(b)
使用Swap置换值时,必须要保持原有的数据类型,否则就会 panic: sync/atomic: swap of inconsistently typed value into Value [recovered]。
需要注意的是,atomic.value 对于复杂的数据结构不能保证原子操作,如切片、映射等。
sync.map
go 在并发下,同时读 map 是安全的,但是读写 map 会引发竞争,导致 panic: fatal error: concurrent map read and map write。
// 创建一个map
m := make(map[int]int)
// 开启两个协程不停的对map写入数据
go func() {
for {
m[1] = 1
}
}()
go func() {
for {
_ = m[1]
}
}()
for {
}
// 结果
fatal error: concurrent map read and map write
为了解决这个问题,可以在写 map 之前加入锁:
l := sync.Mutex{}
l.Lock()
m[1] = 1
l.Unlock()
这样处理程序上运行是没问题了,但是性能并不高。go 在 1.9 版本中加入了效率较高的并发安全:sync.map:
func (m *Map) Store(key, value any) // 储存一个数据
func (m *Map) Load(key any) (value any, ok bool) // 读取一个数据
func (m *Map) Delete(key any) // 删除一个数据
func (m *Map) Range(f func(key, value any) bool) // 遍历数据
实例:
var smap sync.Map
// 保存数据
smap.Store("shanghai", 40000)
smap.Store("nanjing", 10000)
smap.Store("wuhan", 20000)
smap.Store("shenzhen", 30000)
// 读取值
if v, ok := smap.Load("nanjing"); ok {
fmt.Printf("键名:%s,值:%v\n", "nanjing", v)
}
// 删除
smap.Delete("wuhan")
if v, ok := smap.Load("wuhan"); !ok {
fmt.Printf("键名:%s,值:%v\n", "wuhan", v)
}
// 遍历数据
smap.Range(func(k, v interface{}) bool {
fmt.Printf("键名:%s,值:%v\n", k, v)
return true
})
// 结果
键名:nanjing,值:10000
键名:wuhan,值:<nil>
键名:shenzhen,值:30000
键名:shanghai,值:40000
键名:nanjing,值:10000
sync.map 并没有获取长度的方法,只能在遍历的时候自行计算。
本系列文章:
Go 并发编程 - 并发安全(二)的更多相关文章
- 【Java并发编程】之二:线程中断
[Java并发编程]之二:线程中断 使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...
- java并发编程笔记(二)——并发工具
java并发编程笔记(二)--并发工具 工具: Postman:http请求模拟工具 Apache Bench(AB):Apache附带的工具,测试网站性能 JMeter:Apache组织开发的压力测 ...
- Python并发编程-并发解决方案概述
Python并发编程-并发解决方案概述 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.并发和并行区别 1>.并行(parallel) 同时做某些事,可以互不干扰的同一个时 ...
- python并发编程之多进程(二):互斥锁(同步锁)&进程其他属性&进程间通信(queue)&生产者消费者模型
一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终 ...
- python并发编程&多线程(二)
前导理论知识见:python并发编程&多线程(一) 一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 官网链 ...
- java并发编程实战《二》java内存模型
Java解决可见性和有序性问题:Java内存模型 什么是 Java 内存模型? Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为, Java 内存 ...
- 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)
简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...
- 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)
在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...
- 并发编程>>并发级别(二)
理解并发 这是我在开发者头条看到的.@编程原理林振华 有目标的提升自己会事半功倍,前行的道路并不孤独. 1.阻塞 当一个线程进入临界区(公共资源区)后,其他线程必须在临界区外等待,待进去的线程执行完成 ...
- 并发编程入门(二):分析Boost对 互斥量和条件变量的封装及实现生产者消费者问题
请阅读上篇文章<并发编程实战: POSIX 使用互斥量和条件变量实现生产者/消费者问题>.当然不阅读亦不影响本篇文章的阅读. Boost的互斥量,条件变量做了很好的封装,因此比" ...
随机推荐
- django 整合 vue
django 整合 vue 安装 vue 1. 安装 node.js , 官网地址: https://nodejs.org/zh-cn/download/ 2. 使用 npm 淘宝镜像 npm i ...
- 【Java】按钮数组波纹效果
简介 最近Java学到了布局管理器,看到GridLayout就很有意思,老师说可以做Excel表格什么的,心中突发奇想,于是就想做一个波纹状按钮效果(事后一想可能是我键盘光效的影响-.-),网上一搜, ...
- 各种版本的Linux 镜像下载网址
今天发现Linux 镜像下载网址感觉很不错,分享给有需要的小伙伴们 访问地址 Linux操作系统各版本ISO镜像下载(包括oracle linux\redhat\centos\ubuntu\debia ...
- Microsoft R 和 Open Source R,哪一个才最适合你?
由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. R 是一个开源统计软件,在分析领域普及的非常快. 在过去几年中,无论业务规模如何,很多公司都采 ...
- JavaWeb编程面试题——MyBatis
引言 面试题==知识点,这里所记录的面试题并不针对于面试者,而是将这些面试题作为技能知识点来看待.不以刷题进大厂为目的,而是以学习为目的.这里的知识点会持续更新,目录也会随时进行调整. 关注公众号:编 ...
- vue 自己实现一套 keepalive 方案
vue自定义keepalive组件 前一阵来了一个新的需求,要在vue项目中实现一个多开tab页面的功能,本来心想,这不简单嘛就是一个增加按钮重定向吗?(当然如果这么简单我就不写这个文章了).很快写完 ...
- 精彩回顾 | 金蝶云苍穹技术开放日xUser Club广州站
6月14日,以"项目实践案例:性能优化与实践"为主题的金蝶云·苍穹技术开放日广州站圆满落幕.此次活动吸引了50多位开发者到场,大家不仅聆听了开发者关于"代码检查.性能优化 ...
- .NET 5 的烦恼
由于微软工程师的辛勤脑洞和劳作,.NET 生态如何演变完全看他们的决策,其中包含社区吸纳内容.团队讨论结果等等,不乏一些工程师.架构师偏好,很难摸得准. 就比如这一次未来规划,他们希望将 .NET ...
- Linux 上的 .NET 如何自主生成 Dump
一:背景 1. 讲故事 前几天微信上有位朋友找到我,说他程序的 线程数 会偶发性瞬时飙高,让我看下大概是什么原因,截图如下: 如果这种问题每天都会出现,比较好的做法就是用 dotnet-trace 捕 ...
- HCL实验:1.两台PC通过交换机ping通
实验整体的拓扑图 进行交换机配置 配置好PC的ip netmask gatework 接口管理为启用状态 开启SW1 连接的端口 (一般来说是默认打开的,但有时候会自动关闭,很烦,所以最好加上这步) ...