G

[Golang] Select实现优先级并发技巧

RoLingG Golang 2024-09-17

Select实现优先级并发技巧

序言

Go语言中使用 Select 语句可以同时对多个case进行监听,它通常与多个 channel 一起使用去进行协程的资源监听。当 Select 的其中一个 case 满足条件时,Select 就会执行对应的 case 分支。

这里我们写一个简单的程序去运行看看他的特性:

func main() {
    numList := make(chan int, 5)
    strList := make(chan string, 1)
    go func() {
        for i := 0; i < 5; i++ {
            numList <- i
        }
        strList <- fmt.Sprintf("曼波曼波")
    }()
    for {
        select {
        case num := <-numList:
            fmt.Println(num)
        case str := <-strList:
            fmt.Println(str)
            return
        }
    }
}

运行结果:

# 第一次运行结果:
0
1
曼波

# 第二次运行结果:
0
曼波

# 第三次运行结果:
0
1
2
曼波

# 第四次运行结果:
0
1
2
3
4
曼波

我们会发现上面的运行结果基本每次都不一样,这是因为 Select 的特性导致的,SelectSwitch 不同,它并不按照代码书写的顺序去进行判断分支条件是否满足,而是会(伪)随机的选择一个满足条件的 case 分支进行执行。

因此上面的代码并不是先打印完 numList 再打印 strList。上面代码开启了两个协程,因为子协程将两个 channel 需要的值都赋予了给它们,但是当主协程去进行 Select 读取的时候,因为两个 case 都满足条件,会触发 Select 的(伪)随机特性,使得每次运行的结果都可能不一样。

因此,想要在多个满足条件的 case 上先运行我们想要的 case,我们就需要给这部分加个优先级。

优先级

例如我们期望先打印完 numList 再打印 strList 后结束,我们就可以这样写:

func main() {
    numList := make(chan int, 5)
    strList := make(chan string, 1)
    go func() {
        for i := 0; i < 5; i++ {
            numList <- i
        }
        strList <- fmt.Sprintf("曼波曼波")
    }()
    for {
        select {
        case num := <-numList:
            fmt.Println(num)
        case str := <-strList:
            for {
                select {
                case num := <-numList:
                    fmt.Println(num)
                default:
                    fmt.Println(str)
                    return
                }
            }
        }
    }
}

输出结果:

# 输出结果:
0
1
2
3
4
曼波曼波

这样,使用双层 for 循环嵌套去判断就行了,里层 for 循环的 Select 选择,当 numcase 满足条件时,就运行它,不然就运行 str 的默认 case 。因为之前一个 Select 里面两个 case 会存在随机,当里层用了 default 之后,有其他的 case ,就会优先选择其他 case 而不是 default 的内容啦。

额外

具体K8s中也有利用 Select 优先级去处理 nodepod 的优先级更新,优先更新 node 再更新 pod

for {
    select {
    case <-stopCh:
        return
    case nodeUpdate := tc.nodeUpdateChannels[workers]:
        tc.handleNodeUpdate(ctx, nodeUpdate)
        tc.nodeUpdateQueue.Done(nodeUpdate)
    case podUpdate := <- tc.podUpdateChannels[works]:
        //If we found a Pod update we need to empty Node queue first.
    pirority:
        for {
            select {
            case nodeUpdate := tc.nodeUpdateChannels[workers]:
                tc.handleNodeUpdate(ctx, nodeUpdate)
                tc.nodeUpdateQueue.Done(nodeUpdate)
            default:
                // 跳出pirority外,跳出到for循环
                // 如果不这样做,只能break出下一次的Select
                break pirority 
            }
        }
        // After Node queue is emptied we process podUpdate.
        tc.handlePodUpdate(ctx, podUpdate)
        tc.PodUpdateQueue.Done(podUpdate)
    }
}

这里因为要一直轮询优先去查 node 是否更新,高优先级的 node 正常更新,当没有高优先级的 node 准备就绪了,K8s就需要保证清空完 node 的升级,再去更新 pod。(意思就是始终优先更新node

所以里侧 for 循环如果没有 node 就绪更新,要跳出再去更新对应的 pod ,之后触发下一次外层 for 循环一直轮询优先查找 node 是否更新。

总结

至此,这就是Go语言中 Select 的一个优先级的并发技巧。我也只是听别人的讲解学的,感觉有用就记录了下来。

PREV
[面试] 腾讯csig运维实习生面经
NEXT
[Kubernetes] 调度策略及相关内容

评论(0)

发布评论