什么是并发安全

并发情况下,多个线程或协程会同时操作同一个资源,例如变量、数据结构、文件等。如果不保证并发安全,就可能导致数据竞争、脏读、脏写、死锁、活锁、饥饿等一系列并发问题,产生重大的安全隐患,比如12306抢到同一张火车票、多个用户抢到只剩一件库存的商品。而并发安全就是为了避免这些问题。Golang 中有一些原则和工具来保证并发安全,例如:

  1. 遵循“通过通信来共享内存,而不是通过共享内存通信”的理念,尽量使用 channel 来传递数据,而不是使用共享变量。
  2. 如果必须使用共享变量,那么要使用合理的锁来避免数据竞争。
  3. 如果使用锁,要注意锁的粒度和范围,尽量减少锁的持有时间和影响范围,避免死锁和活锁。

关于更为详细的并发安全性:可以参考:理解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.Mutexsync.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

  1. RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁。
  2. 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取资源,使用 RLockRUnlock 加锁解锁。
  3. 写锁会阻止其他 goroutine 进来,读写不论,整个锁住的资源由该 goroutine 独占,使用 LockUnlock 加锁解锁。
  4. 应该只在频繁读取,少量写入的情况下使用读写互斥锁
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 并没有获取长度的方法,只能在遍历的时候自行计算。

本系列文章:

  1. Go 并发编程 - Goroutine 基础 (一)
  2. Go 并发编程 - 并发安全(二)
  3. Go 并发编程 - runtime 协程调度(三)

Go 并发编程 - 并发安全(二)的更多相关文章

  1. 【Java并发编程】之二:线程中断

    [Java并发编程]之二:线程中断 使用interrupt()中断线程 ​ 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...

  2. java并发编程笔记(二)——并发工具

    java并发编程笔记(二)--并发工具 工具: Postman:http请求模拟工具 Apache Bench(AB):Apache附带的工具,测试网站性能 JMeter:Apache组织开发的压力测 ...

  3. Python并发编程-并发解决方案概述

    Python并发编程-并发解决方案概述 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.并发和并行区别 1>.并行(parallel) 同时做某些事,可以互不干扰的同一个时 ...

  4. python并发编程之多进程(二):互斥锁(同步锁)&进程其他属性&进程间通信(queue)&生产者消费者模型

    一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终 ...

  5. python并发编程&多线程(二)

    前导理论知识见:python并发编程&多线程(一) 一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 官网链 ...

  6. java并发编程实战《二》java内存模型

    Java解决可见性和有序性问题:Java内存模型 什么是 Java 内存模型? Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为, Java 内存 ...

  7. 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

    简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...

  8. 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)

    在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...

  9. 并发编程>>并发级别(二)

    理解并发 这是我在开发者头条看到的.@编程原理林振华 有目标的提升自己会事半功倍,前行的道路并不孤独. 1.阻塞 当一个线程进入临界区(公共资源区)后,其他线程必须在临界区外等待,待进去的线程执行完成 ...

  10. 并发编程入门(二):分析Boost对 互斥量和条件变量的封装及实现生产者消费者问题

    请阅读上篇文章<并发编程实战: POSIX 使用互斥量和条件变量实现生产者/消费者问题>.当然不阅读亦不影响本篇文章的阅读. Boost的互斥量,条件变量做了很好的封装,因此比" ...

随机推荐

  1. django 整合 vue

    django 整合 vue   安装 vue 1. 安装 node.js , 官网地址: https://nodejs.org/zh-cn/download/ 2. 使用 npm 淘宝镜像 npm i ...

  2. 【Java】按钮数组波纹效果

    简介 最近Java学到了布局管理器,看到GridLayout就很有意思,老师说可以做Excel表格什么的,心中突发奇想,于是就想做一个波纹状按钮效果(事后一想可能是我键盘光效的影响-.-),网上一搜, ...

  3. 各种版本的Linux 镜像下载网址

    今天发现Linux 镜像下载网址感觉很不错,分享给有需要的小伙伴们 访问地址 Linux操作系统各版本ISO镜像下载(包括oracle linux\redhat\centos\ubuntu\debia ...

  4. Microsoft R 和 Open Source R,哪一个才最适合你?

    由于微信不允许外部链接,你需要点击文章尾部左下角的 "阅读原文",才能访问文中链接. R 是一个开源统计软件,在分析领域普及的非常快. 在过去几年中,无论业务规模如何,很多公司都采 ...

  5. JavaWeb编程面试题——MyBatis

    引言 面试题==知识点,这里所记录的面试题并不针对于面试者,而是将这些面试题作为技能知识点来看待.不以刷题进大厂为目的,而是以学习为目的.这里的知识点会持续更新,目录也会随时进行调整. 关注公众号:编 ...

  6. vue 自己实现一套 keepalive 方案

    vue自定义keepalive组件 前一阵来了一个新的需求,要在vue项目中实现一个多开tab页面的功能,本来心想,这不简单嘛就是一个增加按钮重定向吗?(当然如果这么简单我就不写这个文章了).很快写完 ...

  7. 精彩回顾 | 金蝶云苍穹技术开放日xUser Club广州站

    6月14日,以"项目实践案例:性能优化与实践"为主题的金蝶云·苍穹技术开放日广州站圆满落幕.此次活动吸引了50多位开发者到场,大家不仅聆听了开发者关于"代码检查.性能优化 ...

  8. .NET 5 的烦恼

    由于微软工程师的辛勤脑洞和劳作,.NET 生态如何演变完全看他们的决策,其中包含社区吸纳内容.团队讨论结果等等,不乏一些工程师.架构师偏好,很难摸得准.   就比如这一次未来规划,他们希望将 .NET ...

  9. Linux 上的 .NET 如何自主生成 Dump

    一:背景 1. 讲故事 前几天微信上有位朋友找到我,说他程序的 线程数 会偶发性瞬时飙高,让我看下大概是什么原因,截图如下: 如果这种问题每天都会出现,比较好的做法就是用 dotnet-trace 捕 ...

  10. HCL实验:1.两台PC通过交换机ping通

    实验整体的拓扑图 进行交换机配置 配置好PC的ip netmask gatework 接口管理为启用状态 开启SW1 连接的端口 (一般来说是默认打开的,但有时候会自动关闭,很烦,所以最好加上这步) ...