select-case和time.Ticker的使用注意事项

学习播客:https://studygolang.com/articles/5224

package main

import (
	"fmt"
	"runtime"
	"time"
)

func init() {
	runtime.GOMAXPROCS(runtime.NumCPU())
}

func main() {
	ch := make(chan int, 1024)
	go func(ch chan int) {
		for {
			val := <-ch
			fmt.Printf("val:%d\n", val)
		}
	}(ch)

	tick := time.NewTicker(1 * time.Second)
	for i := 0; i < 20; i++ {
		select {
		case ch <- i:
		case <-tick.C:
			fmt.Printf("%d: case <-tick.C\n", i)
		}	

		time.Sleep(200 * time.Millisecond)
	}
	close(ch)
	tick.Stop()
}

问题出在这个select里面:

select {
case ch <- i:
case <-tick.C:
	fmt.Printf("%d: case <-tick.C\n", i)
}

当两个case条件都满足时,系统会通过一个伪随机算法决定哪个case将被执行
所以当 tick.C 条件满足的那个循环,有概率造成 ch<-i 没有发送
(虽然通道两端没有阻塞,满足发送条件)

个人解析:

for i := 0; i < 20; i++ {
    select {
    // 本质:双case同时发生时,且随机算法执行了第二个case,导致少写了一个i
    case ch <- i:
    case <-tick.C:
        fmt.Printf("%d: case <-tick.C\n", i)
    }   

    time.Sleep(200 * time.Millisecond)
}

这个例子的简化如下:
单case场景:

for i := 0; i < 20; i++ {
    select {
    case ch <- i:
        fmt.Printf("%d: case <-tick.C\n", i)
    }   

    time.Sleep(200 * time.Millisecond)
}

等价于:

for i := 0; i < 20; i++ {
    ch <- i
    fmt.Printf("%d: case <-tick.C\n", i) 
    time.Sleep(200 * time.Millisecond)
}

注意,这里的第一个case铁定发生

双case场景:

for i := 0; i < 20; i++ {
    select {
    case ch <- i:
    case <-tick.C:
        fmt.Printf("%d: case <-tick.C\n", i)
    }   

    time.Sleep(200 * time.Millisecond)
}

不言而喻,要么第一个case发生,要么两个case都发生,基本不会出现第二个case单独出现的场景
因为第一个case每次select循环都满足条件,哪怕不延时

解决方案1:

一旦 tick.C 的 case 被随机到,就多执行一次 ch<-i (多个 case 不通用)

select {
case ch <- i:
case <-tick.C:
	fmt.Printf("%d: case <-tick.C\n", i)
	ch <- i
}

解决方案2:

将 tick.C 的 case 单独放到一个 select 里,并加入一个default(保证不阻塞)

select {
case ch <- i:
}
select {
case <-tick.C:
	fmt.Printf("%d: case <-tick.C\n", i)
//一定要加,不然如果该 for 循环中的所有 case 都不满足,则该 select 阻塞,直到有一个 case 满足条件
default: 
}

两种解决方案的输出都是希望的结果

Guess you like

Origin blog.csdn.net/wangkai6666/article/details/121179179