栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Go语言

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

Go语言 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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=%vn", 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=%vn", 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=%vn", 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) 来将关闭的 chan 置 nil ,当再次执行到 select 时,因为 chan 是 nil 会进入阻塞。

而 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=%vn", 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 循环语句
  • 可以使用 goto 加 lable 跳转到 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=%vn", 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 语句就会被唤醒,这个候选分支就会被执行。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/945649.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号