秒懂 Golang 中的 条件变量(sync.Cond)

本篇文章面向的读者: 已经基本掌握Go中的 协程(goroutine)通道(channel)互斥锁(sync.Mutex)读写锁(sync.RWMutex) 这些知识。如果对这些还不太懂,可以先回去把这几个知识点解决了。

首先理解以下三点再进入正题:

  • Go中的一个协程 可以理解成一个独立的人,多个协程是多个独立的人
  • 多个协程都需要访问的 共享资源(比如共享变量) 可以理解成 多人要用的某种公共社会资源
  • 上锁 其实就是加入到某个共享资源的争抢组中上锁完成 就是从争抢组中被选出,得到了期待的共享资源;解锁 就是退出某个共享资源的争抢组。 

假如有这样一个现实场景:在一个公园中有一个公共厕所,这个厕所一次只能容纳一个人上厕所,同时这个厕所中有个放卷纸的位置,其一次只能放一卷纸,一卷纸的总长度是 5 米,而每个人上一次厕所需要用掉 1 米的纸。而当一卷纸用完后,公园管理员要负责给厕所加上一卷新纸,以便大家可以继续使用厕所。 那么对于这个单人公共厕所,大家只能排队上厕所,当每个人进到厕所的时候,当然会把厕所门锁好,以便任何人都进不来(包括管理员)。管理员若要进到厕所查看用纸情况并加卷纸,也需要排队(因为插队总是不文明对吧)。

那么怎么用 Golang 去模拟上述场景呢?

首先我们先不用 sync.Cond,看如何实现?那么请看下面这段代码:

package main

import (
    "fmt"
    "time"
    "sync"
)

var 卷纸 int
var m sync.Mutex
var wg sync.WaitGroup

func 上厕所(姓名 string){
    m.Lock()
    defer func(){
        m.Unlock()
        wg.Done()
    }()
    fmt.Printf("%s 进到厕所t",姓名)
    if 卷纸 >= 1 {  // 进到厕所第一件事是看还有没有纸
        fmt.Printf("正在拉屎中...n")
        time.Sleep(time.Second)
        卷纸 -= 1
        fmt.Printf("%s 已用完厕所,正在离开n",姓名)
        return
    } 
    fmt.Printf("发现纸用完了,无奈先离开厕所n")
}

func 加厕纸(){
    m.Lock()
    defer func(){
        m.Unlock()
        wg.Done()
    }()
    fmt.Printf("公园管理员 进到厕所t")
    if 卷纸 

  

上面的代码已经能看出一些效果,但还是有问题:最后三个人因为厕纸用完,都直接离开厕所后就没有后续了?应该是他们离开厕所后再次尝试排队,直到需求解决,就离开厕所不再参与排队了,否则要不断去排队上厕所。而公园管理员呢,他要一直去排队进到厕所里看还有没有纸,而不是看一次就再也不管了。 那么请看下面的完善代码:

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    卷纸    int
    m     sync.Mutex
    wg    sync.WaitGroup
    厕所的排队 chan string
)

func 上厕所(姓名 string) {
    m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中;
             // 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中)
    defer func() {
        m.Unlock()
        wg.Done()
    }()
    fmt.Printf("%s 进到厕所t", 姓名)
    if 卷纸 >= 1 { // 进到厕所第一件事是看还有没有纸
        fmt.Printf("正在拉屎中...n")
        time.Sleep(time.Second)
        卷纸 -= 1
        fmt.Printf("%s 已用完厕所,正在离开n", 姓名)
        return
    }
    fmt.Printf("发现纸用完了,无奈先离开厕所n")
    厕所的排队 

  

上面这个代码在功能上基本是完善了,成功模拟了上述 多人上公厕 的场景。但仔细一想,这个场景其实有些地方是不合常理的:如果有个人进到厕所发现没纸,难道他会出来紧接着再去排队吗?如果排了三次五次甚至十次还是没有纸,还要这样不断地反复排队进去出来又排队?而公园管理员,要是这样不断反复排队进厕所查看,那么他这一天其他啥事都干不了。

所以更合理实际的情况应该是:如果一个人进到厕所发现没纸,他应该先去在旁边歇着或在附近干别的,当公园管理员加完纸后,会通过喇叭吆喝一声:“新纸已加上”。这样,附近所有因为没厕纸而歇着的人就会听到这个通知,此时,他们再去尝试排队进厕所;而公园管理员也不用不断去排队进厕所检查纸用完了没有,因为经过升级,厕所加装了一个功能,有一个纸用尽的报警按钮装在纸盒旁边,当上完厕所的人发现纸用完的时候,他会先按下这个报警按钮,再离开厕所。这个报警的声音在整个公园的各处都可以听到,所以管理员无论在哪里干啥,他都能收到这个纸用尽的报警信号,然后他才去进厕所加纸。

其实这种被动通知的模式就是 sync.Cond 的核心思想,它会减少资源消耗,达到更优的效果,下面就是改良为 sync.Cond 的实现代码:

package main

import (
    "fmt"
    "math"
    "strconv"
    "sync"
    "time"
)

var (
    卷纸   int
    m    sync.Mutex
    cond = sync.NewCond(&m)
)

func 上厕所(姓名 string) {
    m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中;
             // 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中)
    defer m.Unlock()
    fmt.Printf("%s 进到厕所t", 姓名)
    for 卷纸  0 { // 管理员进到厕所是看纸有没有用完
        fmt.Printf("发现纸还没用完,先离开厕所在等纸用尽的报警消息n")
        cond.Wait() // 如果纸没用完,就先去干其他工作,等纸用尽的报警消息
        fmt.Printf("公园管理员 等到了纸用尽的报警消息,并再次抢到了厕所n")
    }
    fmt.Printf("公园管理员 正在加新纸...n")
    time.Sleep(time.Millisecond * 500)
    卷纸 = 5
    cond.Broadcast() // 注意:公园管理员加完新纸后,要通过喇叭喊一声 “纸已加上” 的消息通知所有 因没纸而等待上厕所的人
    fmt.Printf("公园管理员 已加上新厕纸,并通过喇叭通知了该消息,并正在离开厕所n")
}

func main() {
    卷纸 = 5  // 厕所一开始就准备好了一卷纸,长度5米
    要上厕所的人 := [...]string{"老王", "小李", "老张", "小刘", "阿明", "欣欣", "西西", "芳芳"} // 上厕所的人名模板
    go func() { // 在这个执行体中,代表厕所及厕所队列的时间线,厕所永远运营下去
        for i := 0; i > 屏幕停止输出后,请按Enter键继续 > 屏幕停止输出后,请按Enter键继续 > 屏幕停止输出后,请按Enter键继续 

  

用了 sync.Cond 的代码显然要精简了很多,而且还节省了计算资源,只会在收到通知的时候 才去抢公共厕所,而不是不断地反复去抢公共厕所。通过这个对现实场景的模拟,我们就很容易从使用者的角度理解 sync.Cond 是什么,它的字面意思就是 “条件”,这就已经点出了这东西的核心要义,就是满足条件才执行,条件是什么,信号其实就是条件,当一个执行体收到信号之后,它才去争抢共享资源,否则就会挂起等待(这种等待底层其实会让出线程,所以这种等待并不会空耗资源),比起不断轮寻去抢资源,这种方式要节省得多。

最后留给读者一个思考的问题:就是上面最后一版的代码,为什么 当纸用完后按报警按钮通知 公园管理员 要用 sync.Broadcast() 方法去广播通知?不是只通知管理员一个人吗,单独通知他不就行了,用 sync.Signal() 为什么不行?

 

文章来源于互联网:秒懂 Golang 中的 条件变量(sync.Cond)

THE END
分享
二维码