有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。Sync包主要实现了并发任务同步WaitGroup的几种方法和并发安全的互斥锁和读写锁方法,还实现了比较特殊的两个方法,一个是保持只执行一次的Once方法和线程安全的Map。

sync.WaitGroup(同步等待)

sync.WaitGroup内部维护着一个计数器Add(),计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1,底层为Add(-1)。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

var x int64
var wg sync.WaitGroupfunc add() {for i := 0; i < 5000; i++ {x = x + 1 //数据竞争}wg.Done()
}
func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

sync.Mutex(互斥锁)

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

var x int64
var wg sync.WaitGroup
var lock sync.Mutexfunc add() {for i := 0; i < 5000; i++ {lock.Lock() // 加锁x = x + 1lock.Unlock() // 解锁}wg.Done()
}
func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

sync.RWMutex(读写互斥锁)

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

读写锁示例:

var (x      int64wg     sync.WaitGrouplock   sync.Mutexrwlock sync.RWMutex
)func write() {// lock.Lock()   // 加互斥锁rwlock.Lock() // 加写锁x = x + 1time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒rwlock.Unlock()                   // 解写锁// lock.Unlock()                     // 解互斥锁wg.Done()
}func read() {// lock.Lock()                  // 加互斥锁rwlock.RLock()               // 加读锁time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒rwlock.RUnlock()             // 解读锁// lock.Unlock()                // 解互斥锁wg.Done()
}func main() {start := time.Now()for i := 0; i < 10; i++ {wg.Add(1)go write()}for i := 0; i < 1000; i++ {wg.Add(1)go read()}wg.Wait()end := time.Now()fmt.Println(end.Sub(start))
}

需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

sync.Once(单例)

说在前面的话:这是一个进阶知识点。

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

sync.Map(线程安全map)

Go语言中内置的map不是并发安全的。请看下面的示例:

var m = make(map[string]int)func get(key string) int {return m[key]
}func set(key string, value int) {m[key] = value
}func main() {wg := sync.WaitGroup{}for i := 0; i < 20; i++ {wg.Add(1)go func(n int) {key := strconv.Itoa(n)set(key, n)fmt.Printf("k=:%v,v:=%v\n", key, get(key))wg.Done()}(i)}wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

var m = sync.Map{}func main() {wg := sync.WaitGroup{}for i := 0; i < 20; i++ {wg.Add(1)go func(n int) {key := strconv.Itoa(n)m.Store(key, n)value, _ := m.Load(key)fmt.Printf("k=:%v,v:=%v\n", key, value)wg.Done()}(i)}wg.Wait()
}

sync/atomic(原子操作)

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。

方法 操作类型
func LoadInt32(addr *int32) (val int32) 读取操作
func LoadInt64(addr *int64) (val int64) 读取操作
func LoadUint32(addr *uint32) (val uint32) 读取操作
func LoadUint64(addr *uint64) (val uint64) 读取操作
func LoadUintptr(addr *uintptr) (val uintptr) 读取操作
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) 读取操作
func StoreInt32(addr *int32, val int32) 写入操作
func StoreInt64(addr *int64, val int64) 写入操作
func StoreUint32(addr *uint32, val uint32) 写入操作
func StoreUint64(addr *uint64, val uint64) 写入操作
func StoreUintptr(addr *uintptr, val uintptr) 写入操作
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) 写入操作
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) 修改操作
func SwapInt32(addr *int32, new int32) (old int32) 交换操作
func SwapInt64(addr *int64, new int64) (old int64) 交换操作
func SwapUint32(addr *uint32, new uint32) (old uint32) 交换操作
func SwapUint64(addr *uint64, new uint64) (old uint64) 交换操作
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) 交换操作
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) 交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) 比较并交换操作
func CompareAndSwapInt64(addr *int64, old, new int64) (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) 比较并交换操作
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) 比较并交换操作

示例:

package mainimport ("fmt""sync""sync/atomic""time"
)var x int64
var l sync.Mutex
var wg sync.WaitGroup// 普通版加函数
func add() {// x = x + 1x++ // 等价于上面的操作wg.Done()
}// 互斥锁版加函数
func mutexAdd() {l.Lock()x++l.Unlock()wg.Done()
}// 原子操作版加函数
func atomicAdd() {atomic.AddInt64(&x, 1)wg.Done()
}func main() {start := time.Now()for i := 0; i < 10000; i++ {wg.Add(1)//go add()       // 普通版add函数 不是并发安全的//go mutexAdd()  // 加锁版add函数 是并发安全的,但是加锁性能开销大go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版}wg.Wait()end := time.Now()fmt.Println(x)fmt.Println(end.Sub(start))
}

并发安全Sync包的使用相关推荐

  1. 4.12 并发技术:sync包同步调度综合案例

    同步调度概述 Go语言的并发中,当使用go关键字开辟若干新的协程时,如果不加干涉,它们会完全并发地得到执行: 而所谓调度,就是在并发的局部植入一些串行和同步的操作,让某些协程有逻辑上的先后关系: 执行 ...

  2. 面试官让我用channel实现sync包里的同步锁,是不是故意为难我?

    前言 Go语言提供了channel和sync包两种并发控制的方法,每种方法都有他们适用的场景,并不是所有并发场景都适合应用channel的,有的时候用sync包里提供的同步原语更简单.今天这个话题纯属 ...

  3. Go语言sync包的应用详解

    在并发编程中同步原语也就是我们通常说的锁的主要作用是保证多个线程或者 goroutine在访问同一片内存时不会出现混乱的问题.Go语言的sync包提供了常见的并发编程同步原语,上一期转载的文章< ...

  4. 从项目的一个 panic 说起:Go 中 Sync 包的分析应用

    项目开发中遇到一个错误 "fatal error: concurrent map read and map write". 有过一两年 Golang 开发经验的同学应该都不陌生,这 ...

  5. sync包——互斥锁

    官网文档对sync包的介绍: Package sync provides basic synchronization primitives such as mutual exclusion locks ...

  6. sync包——读写锁

    官网文档对sync包的介绍: Package sync provides basic synchronization primitives such as mutual exclusion locks ...

  7. GO语言基础进阶教程:sync包——读写锁

    官网文档对sync包的介绍: Package sync provides basic synchronization primitives such as mutual exclusion locks ...

  8. GO语言基础进阶教程:sync包——互斥锁

    官网文档对sync包的介绍: Package sync provides basic synchronization primitives such as mutual exclusion locks ...

  9. go语言sync包的Map源码

    Go语言在sync包中给出了线程安全的map. Map的结构体比较简单. type Map struct {mu Mutex// read contains the portion of the ma ...

最新文章

  1. 微信支付异常:appid and openid not match
  2. 如何读取csv文件中第n行数据python-python数据处理之如何选取csv文件中某几行的数据...
  3. Constructing the Array CodeForces - 1353D(数据结构+分类+建设性算法)
  4. 字魂35号经典雅黑_2020:上海不锈钢黑钛线条行业
  5. 一种简单的LRU cache设计 C++
  6. C++重载流插入运算符与流提取运算符
  7. insert into 多张表_麦克维尔直流变频多联机弹簧阻尼减震器_淞江集团-李工
  8. bzoj2298: [HAOI2011]problem a
  9. 浮点数比较大小常用规则
  10. Java常量池详解:字符串常量池、Class常量池、运行时常量池 三者关系
  11. 使用C#创建快捷方式
  12. 观看影片《硅谷传奇》
  13. vmware克隆linux虚拟机,报Device eth1 does not seem to be present,delaying initialization.错误
  14. 清音驱腐启鸿蒙,中华成语千句文解释.doc
  15. Java经典算法四十例编程详解+程序实例
  16. 输入数字N,然后输入N个数,计算这N个数的和。
  17. 数据库查找姓李的人_数据库基本查询方法等
  18. Ghostscript已有字体报错can‘t find font file问题的原因
  19. java关键字有system吗_求java中的关键字?
  20. BTC100白新学堂——货币流通的双重巧合

热门文章

  1. keyshot渲染图文教程_一篇文章教你学会3D建模和渲染 反正我是信了
  2. 高度平衡二叉树的构建_平衡二叉树建立及其增删改查(JAVA)
  3. 赛迪155页PPT中国“新基建”发展研究报告!
  4. 专业人士提升数据中心职业生涯的6件事
  5. shell开启飞行模式_手机上的飞行模式,每天都能用得上,我是才知道,涨知识...
  6. ML:机器学习中与模型相关的一些常见的判断逻辑(根据自调整阈值计算阳性率)
  7. Py之matplotlibseaborn :matplotlibseaborn绘图的高级进阶之高级图可视化(基础图(直方图等),箱线图、密度图、小提琴图等)简介、案例应用之详细攻略
  8. ML之LoR:基于LoR(逻辑回归)算法对乳腺癌肿瘤(9+1)进行二分类预测(良/恶性)
  9. Python之pyspark:pyspark的简介、安装、使用方法之详细攻略
  10. 深入浅出统计学 第一章 数据的可视化