select| 青训营笔记

86 阅读4分钟

select语句实现优先级| 青训营笔

这是我参与「第五届青训营 」笔记创作活动的第13天

select是go语言的一大特色,case执行顺序是随机的,有时候我们有需求让case有优先级的执行。

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

select语句介绍

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

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

空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读取操作。

总结

  • select 不存在任何的 case:永久阻塞当前 goroutine
  • select 只存在一个 case:阻塞的发送/接收
  • select 存在多个 case:随机选择一个满足条件的case执行
  • 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中就有关于上面这个技巧的实际使用示例

// 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)
      }
   }
}
复制代码

我目前需要这个优先级select的应用场景一般是,当我的程序catch到了异常信号需要退出时,我会在退出前把select下其他chan都执行完了再退出

//异常退出的信号
var kExitSignals = []os.Signal{
    syscall.SIGTERM,
    syscall.SIGINT,
    syscall.SIGSEGV,
    syscall.SIGBUS,
    syscall.SIGQUIT,
}

int main() {
    exitChan := make(chan os.Signal, 1)
    signal.Notify(exitChan, kExitSignals...)
    
    handleCh := make(chan struct{}, 10)
    
    //goroutine with input handleCh 
    
    for {
        select {
        case <-handleCh:
            //do some handle
        case signl := <-exitChan:
            //收到了退出信号,也要保证先把chan中的数据处理了再退出
        priority:
            for {
                select {
                case <-handleCh:
                    // do some handle
                default:
                    break priority
                }
            }
            log.Info("catch signal:", signl)
            //做一些清理工作和资源回收
            //os.Exit(1)
             //如果是服务器的可以使用server.Stop()或者server.GraceStop()来退出
        }
    }
}

复制代码

引用

李文周的博客