goroutine资源竞争

发布时间 2023-04-07 23:00:36作者: yangphp

前言:

如果两个或者多个 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)

 

 

完结