go条件变量的使用和原理

场景

最近写代码时碰到一个场景, 需要使用 map[int]struct{} 结构来存储task, map的key是task的id,随时可以增减。因为的确除了看书,基本上没使用过条件变量所以后面过了一天才想到可以用条件变量来实现。记得在某篇博客上看到的一句话挺不错,大概是同步语句中,条件变量的特点在于等待。

一开始代码大概是这样的

事件循环

func Loop() {
    for {
        mutex.Lock()
        for taskId, task := range tasks {
            // handle code
        }
        lenght := len(task)
        mutex.UnLock()

        // 为了减少cpu空转  当队列为空的时候sleep 2秒
        if length == 2 {
            time.Sleep(time.Secord * 2)
        }
    }
}   

新增task(删除也是类似)

func addTask(t *Task) {
    mutex.Lock()
    tasks[t.Id] = t
    mutex.UnLock()
}

使用条件变量

使用条件变量之后的事件循环代码

func Loop() {
    for {
        mutex.Lock()
        // 如果当前任务数为0 调用Wait()等待新任务增加时唤醒
        if len(tasks) == 0 {
            cond.Wait() // cond := sync.NewCond(&mutex)
        }

        for taskId, task := range tasks {
            // handle code
        }
        mutex.UnLock()
    }
}

新增task(删除task代码不作改变)

func addTask(t *Task) {
    mutex.Lock()
    tasks[t.Id] = t
    if len(task) == 1 {  // 从0->1 可能之前有goruntine阻塞
        cond.Signal()    // 由于Loop()是单协程在跑所以 使用的是Signal()足矣
    }
    mutex.UnLock()
}

条件变量原理(和语言无关)

如果是C语言的pthread_cond条件变量和GO最主要的区别,本质上还是协程和真正的内核线程的区别, go 自带sync包里的条件变量 对goruntine的操作,其阻塞/唤醒不需要陷入内核态。

Wait()

func (c *Cond) Wait() {
    c.checker.check()
    t := runtime_notifyListAdd(&c.notify)  // 等待的goruntine数+1
    c.L.Unlock() // 释放锁资源
    runtime_notifyListWait(&c.notify, t) // 阻塞,等待其他goruntine唤醒
    c.L.Lock() // 获取资源
}

Signa() 和 BroadCast()

func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify) // 唤醒最早被阻塞的goruntine
}

func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify) // 唤醒所有goruntine
}

猜你喜欢

转载自www.cnblogs.com/Me1onRind/p/11653561.html