Go 知识点(09)— for select 作用于 channel

在使用select语句的时候,我们首先需要注意下面几个事情。

  • 有默认分支,那么无论涉及通道操作的表达式是否有阻塞,select 语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。
  • 没有加入默认分支,那么一旦所有的 case 表达式都没有满足求值条件,那么 select 语句就会被阻塞。直到至少有一个 case 表达式满足条件为止。
  • 我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。
  • select 语句只能对其中的每一个 case 表达式各求值一次。如果我们想连续或定时地操作其中的通道的话,就往往需要通过在 for 语句中嵌入 select 语句的方式实现。但这时要注意,简单地在 select 语句的分支中使用 break 语句,只能结束当前的 select 语句的执行,而并不会对外层的 for 语句产生作用。这种错误的用法可能会让这个 for 语句无休止地运行下去。

1. for select 作用于未关闭的通道

1.1 没有 default 分之场景

先看下面代码

func main() {
	ch := make(chan int, 3)
	go func() {
		time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
		ch <- 1
	}()

	for {
		select {
		case v, ok := <-ch:
			fmt.Printf("v=%v, ok=%v\n", v, ok)
			time.Sleep(1 * time.Second)
		}
		fmt.Println("waiting")
	}
}

执行代码输出结果如下:

v=1, ok=true
waiting
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/home/wohu/project/go/src/demo/demo.go:17 +0x82
exit status 2

从结果我们可以看到:

  • 当通道中没有数据时,且 select 语句没有 default 分之时,会一直阻塞在 case 语句中;
  • 当通道中有数据时,case 语句拿到通道里面的值之后,继续执行 select 语句块之外的其余 for 循环体语句;
  • 当通道里面的数据被取走之后,case 语句一直等待从通道中取数据,但是一直没有数据发送过来就会造成死锁;

怎么避免这个死锁问题呢? 这就需要加上 default 分之了。

1.2 有 default 分之场景

查看下面代码

func main() {
	ch := make(chan int, 3)
	go func() {
		time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
		ch <- 1
	}()

	for {
		select {
		case v, ok := <-ch:
			fmt.Printf("v=%v, ok=%v\n", v, ok)
			time.Sleep(1 * time.Second)
		default:
			fmt.Println("通道没有数据")
			time.Sleep(1 * time.Second)
		}
		fmt.Println("waiting")
	}
}

运行代码输出结果如下:

通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道没有数据
waiting
通道没有数据

从结果我们可以看到:

  • 当通道中没有数据时,走 default 分之,default 分之完成后会继续执行 for 循环体的其它语句;
  • 当通道中有数据,则会执行对应的 case 分之;
  • 当通道再一次没有数据时,则继续会执行 default 分之和剩余的其它for 循环体语句,而且会一直死循环执行;

2. for select 作用于关闭的通道

2.1 对关闭的通道执行 case 会造成死循环

继续下面代码

func main() {
	ch := make(chan int, 3)
	go func() {
		time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
		ch <- 1
		close(ch)
	}()

	for {
		select {
		case v, ok := <-ch:
			fmt.Printf("v=%v, ok=%v\n", v, ok)
			time.Sleep(1 * time.Second)
		default:
			fmt.Println("通道没有数据")
			time.Sleep(1 * time.Second)
		}
		fmt.Println("waiting")
	}
}

运行输出结果

通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
v=0, ok=false
waiting
v=0, ok=false
...

注意我们在前面已经将通道关闭,这个时候的 case 语句依然成立,所以会形成死循环执行这个 case 语句。

那么怎样能跳出这个死循环的 case 语句呢?

2.2 跳出死循环的 case 语句

要跳出这个死循环的 case 语句,我们需要在 case 中通过第二个参数判断 chan 是否关闭,如果关闭则通过 make(chan type) 来将关闭的 channil ,当再次执行到 select 时,因为 channil 会进入阻塞。

select 中如果任意某个分之可读(包括 default ),它就会被执行,其他被忽略。所以在有 default 分之场景时, select 会跳过这个阻塞 case ,去执行 default 分之,这样就可以避开这个死循环的 case 分之。

func main() {
	ch := make(chan int, 3)
	go func() {
		time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
		ch <- 1
		close(ch)
	}()

	for {
		select {
		case v, ok := <-ch:
			if !ok {
				ch = make(chan int)
				fmt.Println("通道已经关闭")
			} else {
				fmt.Printf("v=%v, ok=%v\n", v, ok)
				time.Sleep(1 * time.Second)
			}

		default:
			fmt.Println("通道没有数据")
			time.Sleep(1 * time.Second)
		}
		fmt.Println("waiting")
	}
}

输出结果如下:

通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道已经关闭
waiting
通道没有数据
waiting
...

会一直循环打印 default 分之的输出,那怎样跳出这个循环呢?

2.3 跳出 for select 循环语句

  • 可以使用 gotolable 跳转到 for 外面;
  • 可以设置一个额外的标记位,当 chan 关闭时,设置 flag=true ,在 for 的最后判断 flag 决定是否 break

我们采用第二种方案:

func main() {
	ch := make(chan int, 3)
	go func() {
		time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
		ch <- 1
		close(ch)
	}()

	exitFlag := false
	for {
		select {
		case v, ok := <-ch:
			if !ok {
				ch = make(chan int)
				fmt.Println("通道已经关闭")
				exitFlag = true
			} else {
				fmt.Printf("v=%v, ok=%v\n", v, ok)
				time.Sleep(1 * time.Second)
			}

		default:
			fmt.Println("通道没有数据")
			time.Sleep(1 * time.Second)
		}

		if exitFlag {
			fmt.Println("跳出循环")
			break
		}
		fmt.Println("waiting")
	}
}

输出结果

通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道已经关闭
跳出循环

由以上示例我们可以得出以下结论:

  1. select 语句中如果任意某个 case 的通道有值可读时,它就会被执行,其他 case 会被忽略;
  2. 如果没有 default 语句,select 将有可能阻塞,直到某个 case 分之有值可以运行,所以 select 里最好有一个 default ,否则将有一直阻塞的风险;

如果 select 语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。

仅当 select 语句中的所有 case 表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么 select 语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select 语句就会被唤醒,这个候选分支就会被执行。

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值