goroutine资源竞争
前言:
如果两个或者多个 goroutine ,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。
一个工具帮助我们检查是否存在共享资源竞争的问题
go build -race
正文:
go语言中多个协程操作一个变量时会出现冲突的问题,这种情况会发生竞态问题(数据竞态)
Go语言包中的 sync (https://go-zh.org/pkg/sync/) 包提供了两种锁类型:
sync.Mutex 互斥锁
sync.RWMutex 读写互斥锁
Mutex (互斥锁)是最简单的一种锁类型,
同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。
RWMutex(读写互斥锁) 相对友好些,是经典的单写多读模型。
在读锁占用的情况下,会阻止写,但不阻止读,也就是多个 goroutine 可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他 goroutine(无论读和写)进来
互斥锁的使用场景
1、多个goroutine访问一个代码段
2、这个函数操作一个全局变量
3、为了保证共享变量安全性,值合法性
可以有有多个读锁,只能有一个写锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁
互斥锁实例:
var lock sync.Mutex //初始化互斥锁, var wg = sync.WaitGroup{} var user = make(map[int]int) //定义user 全局变量 var sum int //定义sum全局变量 func AllCharge(n int) { defer wg.Done() println(n) lock.Lock() //锁上 sum = 0 for i := 0; i <= n; i++ { sum += i //这里多个协程操作 一个sum,会发生资源竞争 } user[n] = sum //多个协程操作 user ,会发生资源竞争 lock.Unlock() //解锁 } main: for i := 1; i <= 100; i++ { wg.Add(1) go AllCharge(i) } wg.Wait() fmt.Println(user)
sync.Map
sync.Map 为了保证并发安全会有一些性能损失,
因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
var sm sync.Map //定义
sm.Store(key, val) //存值
v,_ :=sm.Load(key) //取值
sm.Delete(key) //删除
sm.Range(func(key, value any) bool { //遍历
fmt.Println(key, value)
return true
})
sync.Map实例1:
var wg = sync.WaitGroup{} var sm sync.Map //使用sync.Map 并发情况下,比map更好的性能 func AllCharge(n int) { defer wg.Done() sum := 0 for i := 0; i <= n; i++ { sum += i } sm.Store(n, sum) //将值存储到 sync.Map中 } func main() { for i := 1; i <= 100; i++ { wg.Add(1) go AllCharge(i) } wg.Wait() //遍历 sync.Map sm.Range(func(key, value any) bool { fmt.Println(key, value) return true }) //使用 sm.Load 取值 fmt.Println(sm.Load(100)) //5050 true }
sync.Once 设置某段代码只执行一次
go语言标准库中的sync.Once可以保证go程序在运行期间的某段代码只会执行一次, 例如只加载一次配置文件、只关闭一次通道,单例模式。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
语法:
var once sync.Once //定义 once
once.Do(testOnce) //调用其他函数
sync.Once实例1:
var wg sync.WaitGroup var once sync.Once func test(n int) { defer wg.Done() once.Do(testOnce) //once.Do执行的函数不能设置参数 fmt.Println("hello test", n) } func testOnce() { fmt.Println("testOnce") } func main() { for i := 0; i < 5; i++ { wg.Add(1) go test(i) } wg.Wait() }
sync.once 实现单例实例:
不论调用多少次,实例化只执行一次
// 定义单例模式 type singletion struct{} var once sync.Once var intance *singletion func GetInstanc() *singletion { once.Do(func() { fmt.Println("hello once ") intance = new(singletion) //只执行了一次 }) fmt.Println("hello GetInstanc ") //执行了两次 return intance } func main() { ins := GetInstanc() //调用了两次 ins1 := GetInstanc() //调用了两次 fmt.Printf("%T\n", ins) fmt.Printf("%T", ins1) }
原子操作:
原子操作即是进行过程中不能被中断的操作,而锁机制的底层是基于原子操作的,其一般直接通过CPU指令实现。Go语言中原子操作由内置的标准库sync/atomic提供
atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
Go语言提供的原子操作都是非入侵式的
原子操作语法:
Add(增加或减少)
对一个数值进行增加或者减少的行为也需要保证是原子的,它对应于atomic包的函数就是
AddInt64(addr *int64, delta int64) (new int64)
atomic.AddInt64(&a, 1) 增加1
atomic.AddInt64(&a, -4)减少4
Load(原子读取)
当我们要读取一个变量的时候,很有可能这个变量正在被写入,这个时候,我们就很有可能读取到写到一半的数据。 所以读取操作是需要一个原子行为的。
LoadInt64(addr *int64) (val int64)
Store(原子写入)
读取是有原子性的操作的,同样写入atomic包也提供了相关的操作包。
StoreInt64(addr *int64, val int64)
原子操作实例1:使用锁,模拟减少库存操作
不加锁的话,最后结果不为0,是因为同时多个协程操作,会发生异常
var lock sync.Mutex //互斥锁, var wg = sync.WaitGroup{} var sum int64 = 1000 func Desc() { defer wg.Done() lock.Lock() //加锁,如果不加锁的话,最后结果不为0 sum -= 1 lock.Unlock() //操作完毕,解锁 } main: for i := 1; i <= 1000; i++ { wg.Add(1) go Desc() } wg.Wait() fmt.Println(sum) //输出0
原子操作示例2:使用sync atomic.AddInt64
var lock sync.Mutex //互斥锁, var wg = sync.WaitGroup{} var sum int64 = 1000 func Desc() { defer wg.Done() atomic.AddInt64(&sum, -1) //每次减少1 } func main() { for i := 1; i <= 1000; i++ { wg.Add(1) go Desc() } wg.Wait() fmt.Println(sum) }
原子操作实例3:使用sync atomic.StoreInt64
使用一个线程存数字,另外一个线程取数字
var wg = sync.WaitGroup{} var sum int64 = 0 func Wsum(n int64) { defer wg.Done() atomic.StoreInt64(&sum, n) } func Rsum() { defer wg.Done() num := atomic.LoadInt64(&sum) fmt.Println("sum:", num) } 调用: var i int64 for i = 0; i <= 10; i++ { wg.Add(2) go Wsum(i) go Rsum() } wg.Wait() fmt.Println(sum)
完结
但行好事,莫问前程!
本文来自博客园,作者:yangphp,转载请注明原文链接:https://www.cnblogs.com/ypeih/p/17297635.html