39

Go语言在select语句中实现优先级

 3 years ago
source link: https://www.liwenzhou.com/posts/Go/priority_in_go_select/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文回顾了Go语言中select语句的一些用法,并延伸出了一个如何在select中实现优先级的小技巧,希望能对大家有所帮助。

Go语言在select语句中实现优先级

select语句介绍

Go 语言中的 select语句用于监控并选择一组case语句执行相应的代码。它看起来类似于switch语句,但是select语句中所有case中的表达式都必须是channel的发送或接收操作。一个典型的select使用示例如下:

select {
case <-ch1:
	fmt.Println("liwenzhou.com")
case ch2 <- 1:
	fmt.Println("q1mi")
}

Go 语言中的 select 关键字也能够让当前 goroutine 同时等待ch1 的可读和ch2的可写,在ch1ch2状态改变之前,select 会一直阻塞下去,直到其中的一个 channel 转为就绪状态时执行对应case分支的代码。如果多个channel同时就绪的话则随机选择一个case执行。

除了上面展示的典型示例外,接下来我们逐一介绍一些select的特殊示例。

空select

select指的是内部不包含任何case,例如:

select{
  
}

空的 select 语句会直接阻塞当前的goroutine,使得该goroutine进入无法被唤醒的永久休眠状态。

只有一个case

如果select中只包含一个case,那么该select就变成了一个阻塞的channel读/写操作。

select {
case <-ch1:
	fmt.Println("liwenzhou.com")
}

上面的代码,当ch1可读时会执行打印操作,否则就会阻塞。

有default语句

如果select中还可以包含default语句,用于当其他case都不满足时执行一些默认操作。

select {
case <-ch1:
	fmt.Println("liwenzhou.com")
default:
	time.Sleep(time.Second)
}

上面的代码,当ch1可读时会执行打印操作,否则就执行default语句中的代码,这里就相当于做了一个非阻塞的channel读取操作。

  1. select 不存在任何的 case:永久阻塞当前 goroutine
  2. select 只存在一个 case:阻塞的发送/接收
  3. select 存在多个 case:随机选择一个满足条件的case执行
  4. select 存在 default,其他case都不满足时:执行default语句中的代码

如何在select中实现优先级

已知,当select 存在多个 case时会随机选择一个满足条件的case执行。

现在我们有一个需求:我们有一个函数会持续不间断地从ch1ch2中分别接收任务1和任务2,

如何确保当ch1ch2同时达到就绪状态时,优先执行任务1,在没有任务1的时候再去执行任务2呢?

高级Go语言程序员小明挠了挠头写出了如下函数:

func worker(ch1, ch2 <-chan int, stopCh chan struct{}) {

	for {
		select {
		case <-stopCh:
			return
		case job1 := <-ch1:
			fmt.Println(job1)
		default:
			select {
			case job2 := <-ch2:
				fmt.Println(job2)
			default:
			}
		}
	}
}

上面的代码通过嵌套两个select实现了”优先级”,看起来是满足题目要求的。但是这代码有点问题,如果ch1ch2都没有达到就绪状态的话,整个程序不会阻塞而是进入了死循环。

怎么办呢?

小明又挠了挠头,又写下了另一个解决方案:

func worker2(ch1, ch2 <-chan int, stopCh chan struct{}) {
	for {
		select {
		case <-stopCh:
			return
		case job1 := <-ch1:
			fmt.Println(job1)
		case job2 := <-ch2:
		priority:
			for {
				select {
				case job1 := <-ch1:
					fmt.Println(job1)
				default:
					break priority
				}
			}
			fmt.Println(job2)
		}
	}
}

这一次,小明不仅使用了嵌套的select,还组合使用了for循环和LABEL来实现题目的要求。上面的代码在外层select选中执行job2 := <-ch2时,进入到内层select循环继续尝试执行job1 := <-ch1,当ch1就绪时就会一直执行,否则跳出内层select

实际应用场景

上面的需求虽然是我编的,但是关于在select中实现优先级在实际生产中是有实际应用场景的,例如K8s的controller中就有关于上面这个技巧的实际使用示例,这里在关于select中实现优先级相关代码的关键处都已添加了注释,具体逻辑这里就不展开细说了。

// kubernetes/pkg/controller/nodelifecycle/scheduler/taint_manager.go 
func (tc *NoExecuteTaintManager) worker(worker int, done func(), stopCh <-chan struct{}) {
	defer done()

	// 当处理具体事件的时候,我们会希望 Node 的更新操作优先于 Pod 的更新
	// 因为 NodeUpdates 与 NoExecuteTaintManager无关应该尽快处理
	// -- 我们不希望用户(或系统)等到PodUpdate队列被耗尽后,才开始从受污染的Node中清除pod。
	for {
		select {
		case <-stopCh:
			return
		case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
			tc.handleNodeUpdate(nodeUpdate)
			tc.nodeUpdateQueue.Done(nodeUpdate)
		case podUpdate := <-tc.podUpdateChannels[worker]:
			// 如果我们发现了一个 Pod 需要更新,我么你需要先清空 Node 队列.
		priority:
			for {
				select {
				case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
					tc.handleNodeUpdate(nodeUpdate)
					tc.nodeUpdateQueue.Done(nodeUpdate)
				default:
					break priority
				}
			}
			// 在 Node 队列清空后我们再处理 podUpdate.
			tc.handlePodUpdate(podUpdate)
			tc.podUpdateQueue.Done(podUpdate)
		}
	}
}

本文回顾了Go语言中select语句的一些用法,并延伸出了一个如何在select中实现优先级的小技巧,希望能对大家有所帮助。

最后多嘴一句,Go语言由于自身没有很多奇怪的语法糖和自带代码格式化,相比其他语言来说并不会存在看不懂别人写的代码的情况。所以我们完全可以通过阅读优秀库的源代码,与巨人为伍,与高朋为伴,最终吃上更好的饭。


扫码关注微信公众号

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK