ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【Go同步原语】

2021-12-14 10:04:43  阅读:179  来源: 互联网

标签:10 同步 协程 sum sync 原语 func Go 执行


在Go语言中,不仅有channel这类比较易用且高级的同步机制,还有sync.Mutex 、sync.WaitGroup等比较原始的同步机制。通过它们,我们可以更加灵活的控制数据的同步和多协程的并发。

资源竞争

​ 在一个goroutine中,如果分配的内存没有被其他goroutine访问,之后在该goroutine中是哟和哪个,那么不存在资源竞争问题。但如果同一块内容被多个goroutine同时访问,就会产生不知道谁先访问也就无法预料结果。这就是资源竞争,这块内存也可以称为共享资源

// 共享资源
sum := 0

func main () {
    // 开启100个协程 sum+10
    for i:=0; i <100; i++ {
        go add(10)
    }
    
    // 防止提前退出
    tiem.Sleep(2 * time.Second)
    
    fmt.Println("和为:", sum)
}

func add(i int) {
    sum += i
}

// 示例中,结果可能是990或者980,并不是预期的1000。导致这种情况发送的核心原因是sum并不是并发安全的,因为同时有多个协程交叉执行 sum += i,产生不可预料的结果。

小技巧:使用go build、go run、go test这些Go语言工具链提供的命令式,添加-race标记可以帮我们检查Go语言代码是否存在资源竞争。

同步原语

sync.Mutex

​ 互斥锁,顾名思义,指的是同一时刻只有一个协程执行某段代码,其它协程都要等待该协程执行完才能继续执行。 改造上述示例,声明一个互斥锁mutex,修改add函数,访问共享资源的代码片段就并发安全了。

sum:= 0
mutex := sync.Mutex


func add(i int) {
    mutex.Lock()
    defer mutex.Unlock()
    sum += i
}

// 被解锁保护的sum += i 代码片段又称为临界区。在同步原语设计中,临界区指的是一个访问共享资源的代码片段,而这些共享资源又有无法同时被多个协程访问的特征。当有协程进入临界区段时,其它协程必须等待,这样就保证了临界区的并发安全。

​ 互斥锁的使用非常简单,它只有两个方法Lock()和Unlock(),代表加锁和解锁。当一个协程获得Mutex锁后,其它协程只能等到Mutex锁释放后才能再次获得锁。

​ Muetx 的 Lock 和 Unlock 方法总是成对出现,而且要确保Lock 获得锁后,一定执行Unlock释放锁,所以在函数或方法中会 采用defer 语句释放锁。 这样可以确保所以丁被释放,不会被遗忘。

sync.RWMutex

​ sync.Muetx对共享资源进行了加锁,保证并发安全。如果读取操作也是多个协程呢?

func main() {
    for i := 0; i < 100; i ++ {
        go add(10)
    }
    
    for i := 1; i <10 ; i++ {
        go fmt.Println("和为:", getSum())
    }
    
    time.Sleep(2 * time.Second)
}

func getSum() int {
    b := sum
    return b
}

// 开启了10个协程,同时读取sum的值。因为getSum函数没有任何加锁控制,所以它不是并发安全的,即一个goroutine在执行sum+=i 操作时,另一个goroutine可能在执行 b:=sum 操作,这就会导致读取的sum值是一个过期的值,结果不可预期, 要解决资源竞争问题,可以使用互斥锁sync.Mutex

func getSum() int {
    mutex.Lock()
    defer mutex.Unlock()
    b := sum 
    return b
}

​ 因为add 和 getSum函数使用的是同一个sync.Muetx,所以它们的操作是互斥的,也就是一个goroutine进行修改操作时,另一个goroutine读取操作会等待,直到修改操作完毕。

​ 我们解决了goroutine同时读写的资源竞争问题,但是又遇到了另一个问题——性能。因为每次读写共享资源都要加锁,所以性能底下,如何解决呢?

​ 读写这个特殊的场景,有以下几种情况:

1. 写的时候不能同时读,因为这个时候读取的话可能读到脏数据
2. 读的时候不能同时写,因为也可能产生不可预料的结果
3. 读的时候可以同时读,因为数据不会改变,所以不管多少个goroutine读都是并发安全的

​ 所以可以通过sync.RWMutex 来优化此段代码,提升性能。改用读写锁,来实现我们想要的结果

var mutex snyx.RWMutex

func getSum() int {
    // 获取读写锁
    mutex.RLock()
    defer mutex.RUnlock()
    b := sum
    return b
}

// 这样性能会有很大提升,因为多个goroutine可以同时读取数据,不再相互等待。

sync.WaitGroup

​ 我们上述的示例中使用了time.Sleep函数, 是为了防止主函数main() 返回,一旦main函数范湖了, 程序也就退出了。 提示:一个函数或方法的返回(return) 也就意味着当前函数执行完毕。

​ 如我们我们执行100个add协程和10个getSum协程,不知道什么时候执行完毕,所以设置了比较长的等待时间,但是存在一个问题,如果这110个协程很快就执行完毕,main函数应该提前返回,但是还要等待一会才能返回,会产生性能问题。但是如果等待时间截止时,协程没有执行完毕,程序会提前退出,导致有协程没有执行完,产生不可预知的结果。

​ 如何解决这个问题呢? 有没有办法监听所有协程的执行,一旦全部执行完毕,程序马上退出,这样既可保证所有协程执行完毕,又可以及时退出节省时间,提升性能。Channel可以解决这个问题,不过很复杂。Go语言提供了更简洁的方法,它就是 sync.WaitGroup

func run() {
    var wg sync.WaitGroup
    wg.Add(110)
    
    for i := 0; i< 100; i ++ {
        go func() {
          // 计数器减1
            defer wg.Done()
            add(10)
        }()
    }
    
    for i := 1; i < 10 ; i++ {
        go func() {
            defer wg.Done()  
            fmt.Println("sum is", getSum())
        }()
    }
    
    // 一直等待,直到计数器值为0
    wg.Wait()
}

sync.WaitGroup使用比较简单,一共分为三步:

  1. 声明一个sync.WaitGroup,然后通过Add方法设置计数器的值,需要跟踪多少个协程就设置多少
  2. 在每个协程执行完毕时调用Done方法,让计数器减1,告诉sync.WaiGroup 该协程已经执行完毕
  3. 最后调用Wait方法一直等待,直到计数器值为0,也就是所有跟踪的协程都执行完毕

​ 通过sync.WaitGroup可以很好地跟踪协程。在协程执行完毕后,整个函数才会执行完毕,时间不多不少,正好是协程执行的时间。

sync.WaitGroup适合协调多个协程共同做一件事情的场景,比如下载一个文件,假设使用10个协程,每个协程写在1/10,只有10个协程都下载好了整个文件才下载好了。这就是我们经常听说的多线程下载,通过多个线程共同做一件事情,显著提高效率。小提示我们可以把Go语言的协程理解为平常说的线程,从用户体验上并无不可,但是从技术实现上,它们是不一样的。

sync.Once

​ 在实际工作中,可能会有这样的需求:让代码只执行一次,哪怕是高并发情况下,比如创建一个单例。针对这种情形,Go语言为我们提供了sync.Once来保证代码只执行一次。

func main() {
    doOnce()
}

func doOnce() {
    var once sync.Once 
    onceBody := func() {
        fmt.Println("Only once")
    }
    
    // 用于等待协程执行完毕
    done := make(chan bool)
    
    // 启动10个协程执行once.Do(onceBody)
    for i := 1; i < 10; i++ {
        go func(){
            go func() {
                // 把要执行的函数(方法)作为参数传给once.Do方法即可
                once.Do(onceBody)
                done <- true
            }
        }()
    }
    
    for i := 10; i < 10 ; i++{
        <- done
    }
}

​ 这是Go语言自带的一个示例,虽然启动了10个协程来执行onceBody函数,但是因为用了once.Do方法,所以函数onceBody只会执行一次。也就是在高并发的情况下,sync.Once也会保证onceBody函数只执行一次。

​ sync.Once 适用于创建某个对象的单例、只加载一次的资源等只执行一次的场景。、

sync.Cond

​ 在Go语言中,sync.WaitGroup用于最终完成的场景,关键点在于一定要等待所有协程都执行完毕。而sync.Cond 可以用于发号施令,一声令下所有协程都可以开始执行,关键点在于协程开始的时候是等待的,要等待sync.Cond唤醒才能执行。

​ sync.Cond从字面意思看是条件变量,它具有阻塞协程和唤醒协程的功能,所有可以在满足一定条件的情况下唤醒协程,但条件变量只是它的一种使用场景。

// 10个人赛跑,1人裁判发号施令
func race() {
    cond := sync.NewCond(&sync.Mutex{})
    var wg sync.WaitGroup
    wg.Add(11)
    for i := 1; i < 10; i++ {
        go func(num int) {
            defer wg.Done() 
            fmt.Println(num, "号已就位")
            cond.L.Lock()
            cond.Wait() // 等待发令枪响
            fmt.Println(num, "号开始跑")
            cond.L.Unlock()
        }(i)
    }
    
    time.Sleep(2 * time.Second)
    go func(){
        defer wg.Done()
        fmt.Println("裁判已就位,准备发令枪")
        fmt.Println("比赛开始,大家开始跑")
        cond.Broadcast() // 发令枪响
    }()
    
    wg.wait()
}

大概步骤:

  1. 通过sync.NewCond函数生成一个*sync.Cond,用于阻塞和唤醒协程
  2. 然后启动10个协程模拟10个人,准备就位后调用cond.Wait()方法阻塞当前协程等待发令枪响,这里需要注意的是调用cond.Wait()方法是要加锁
  3. time.Sleep 用于等待所有人都进入wait状态,这样裁判才能调用cond.Broadcast()发号施令
  4. 裁判准备完毕后,可以调用cond.Broadcast通知所有人开始跑了

sync.Cond 有三个方法,分别是:

  1. Wait,阻塞当前协程,知道被其他协程调用Broadcast 或者 Signal 方法唤醒,使用的时候需要加锁,使用sync.Cond中的锁即可,也就是L字段
  2. **Signal **,唤醒一个等待时间最长的协程
  3. Broadcast,唤醒所有等待的协程

注意:在调用signal 或者Broadcast之前,要确保目标协程处于Wait阻塞状态,不然会出现死锁问题。

小结:我们了解了Go语言的同步原语使用,通过它们可以更灵活的控制多协程的并发。从使用上讲,Go语言还是更推荐channel这种更高级别的并发控制方式,因为它更简洁,也更容易理解和使用。同步原语通常用于更复杂的并发控制,如果追求更灵活的控制方式和性能,可以使用它们。


标签:10,同步,协程,sum,sync,原语,func,Go,执行
来源: https://blog.csdn.net/zhw21w/article/details/121917593

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有