【Go语言】【18】GO语言的select

qingkechina · · 1021 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、select

Go语言引入了select关键字,其语法与switch非常类似,先看一个switch例子:

func main() {

聽 聽 聽 聽 var a int = 1

聽 聽 聽 聽 switch {

聽 聽 聽 聽 聽 聽 聽 聽 case a == 1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ok")

聽 聽 聽 聽 聽 聽 聽 聽 case a == 2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("no ok")

聽 聽 聽 聽 聽 聽 聽 聽 default:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("default")

聽 聽 聽 聽 }

}

运行该程序,可以正常打印出“ok”

下面把switch替换为select

func main() {

聽 聽 聽 聽 var a int = 1

聽 聽 聽 聽聽select{

聽 聽 聽 聽 聽 聽 聽 聽 case a == 1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ok")

聽 聽 聽 聽 聽 聽 聽 聽 case a == 2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("no ok")

聽 聽 聽 聽 聽 聽 聽 聽 default:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("default")

聽 聽 聽 聽 }

}

运行该程序抛出错误:a == 1 evaluated but not used

这是为何?是因为select中的case语句必须是一个IO操作!!!

即:switch中的case语句判断条件只要能比较就行,而select中的case语句判断条件必须是一个IO操作。


我们在《Go语言的并发》中说过Channel,从Channel中读取值和向Channel中写入值对应的操作都是IO操作。下面我们写一个关于select的例子:

func setValue(ch1 chan int, ch2 chan string) {

聽 聽 聽 聽 ch1 <- 1

聽 聽 聽 聽 ch2 <- "goroutine"

}

聽 聽 聽 聽定义一个函数setValue(),入参为两个channel,该方法体内向channel 1中写入一个整形值、向channel 2中写入一个字符串;在并发章节我们说过:“当入以为channel时,就不是值传递了,而变成一个地址传递”。

func main() {

聽 聽 聽 聽 var ch1 chan int = make(chan int)

聽 聽 聽 聽 var ch2 chan string = make(chan string)

聽 聽 聽 聽 go setValue(ch1, ch2)


聽 聽 聽 聽 select {

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch1 ok")

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch2 ok")

聽 聽 聽 聽 聽 聽 聽 聽 聽 default:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("default")

聽 聽 聽 聽 }

}

聽 聽 聽 聽 先定义两个channel类型的变量,使用make对其初始化;然后使用go关键字拉启一个goroutine;main所在goroutine继续向下走,开始执行select。

执行一下程序:

wKioL1XGzffjzGR0AACJ40MJD08385.jpg

发现只打印了一个“default”,而没有打印“ch1 ok”或者“ch2 ok”,多执行几次结果一样,从并发的角度上考虑概率情况,这是不正常的。


您可能会想向ch1、ch2中写入数据,属于IO操作,可能会慢一些,当select执行完default时,IO操作依旧没有完成。我们验证一下,修改代码:

func main() {

聽 聽 聽 聽 var ch1 chan int = make(chan int)

聽 聽 聽 聽 var ch2 chan string = make(chan string)

聽 聽 聽 聽 go setValue(ch1, ch2)


聽 聽 聽 聽 select {

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch1 ok")

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch2 ok")

聽 聽 聽 聽 }

}

这里把default删除掉了,当执行到select时,程序会查看各个分支,由于没有default分支,此时若channel中没有内容,则main所在的goroutin会阻塞,至到ch1或者ch2中有内容为至。


执行一下该程序:

wKioL1XG0PPSUmstAAC84L3mw_g272.jpg

发现成功打印出"ch1 ok”,多运行几次依旧打印出“ch1 ok”,从没有打印出“ch2 ok”,这是为什么呢?


看过我前面章节的可能会想,由于目前使用的是go1.4版本,它并不支持多核并发,加之GO语言在执行时更倾向先让一个goroutine执行完(即我们常说的让领导先走:) ),下面我们再修改一下程序:

func main() {

聽 聽 聽 聽 runtime.GOMAXPROCS(runtime.NumCPU()) 聽// 强制Go进行多核并发


聽 聽 聽 聽 var ch1 chan int = make(chan int)

聽 聽 聽 聽 var ch2 chan string = make(chan string)

聽 聽 聽 聽 go setValue(ch1, ch2)


聽 聽 聽 聽 select {

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch1 ok")

聽 聽 聽 聽 聽 聽 聽 聽 聽 case <-ch2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch2 ok")

聽 聽 聽 聽 }

}

再多运行几次程序:

wKioL1XG1VOQ0tMyAAEJlkJS3v4955.jpg


从运行结果上来看,多核也没有解决这个问题,这是因为当执行到select时,它发现ch1、ch2均无内容,程序发生阻塞,直到另外的goroutine把数据写入ch1或ch2,由于在另外的goroutine中ch1总是第一个被写入数据,所以main所在的gorouinte总是先从ch1中获取到数据,从而打印“ch1 ok”之后就退出了!


那如何完善这个程序呢?

func setValue(ch chan int){

聽 聽 聽 聽 聽ch <- 1

}


func main() {

聽 聽 聽 聽 runtime.GOMAXPROCS(runtime.NumCPU())


聽 聽 聽 聽 var ch1, ch2 chan int = make(chan int), make(chan int)

聽 聽 聽 聽 go setValue(ch1)

聽 聽 聽 聽 go setValue(ch2)


聽 聽 聽 聽 select {

聽 聽 聽 聽 聽 聽 聽 聽 case <-ch1:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch1 ok")

聽 聽 聽 聽 聽 聽 聽 聽 case <-ch2:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 fmt.Println("ch2 ok")

聽 聽 聽 聽 }

}聽 聽 聽 聽

告诉Go的运行环境当前是多核编程,同时起2个goroutine在多核的状态下运行,这样向channel1、channel2中写入数据顺序就变得不可预测了所以有可能先打印“ch1 ok”,也有可能先打印“ch2 ok”。

多运行几次看一下结果:

wKiom1XR6FXBCYTuAADsARgrzTU715.jpg

从结果上来看,达到了预期目的!


二、死锁

聽 聽 聽 聽如上面的setValue()方法所示,入参是一个channel,方法体是向该channel中写入数据,由于channel作为入参是一个地址传递,所以在select中的case始终能从channel中读取到数据。

聽 聽 聽 聽试想若程序猿把setValue()中赋值忘记了呢?如下:

func setValue(ch chan int) {

聽 聽 聽 聽 // ch <- 1注释掉该行

}

运行一下发现系统报了死锁:

wKioL1XR7I7g1U1dAADZswIIZPI327.jpg


像这种情况是在所难免的,如果避免死锁这类问题呢?

有一种办法是引入另一个超时Channel,另启一个goroutine先让它休息一定时间(超时时间),然后把数据写入该Channel,代码如下:

package main


import (

聽 聽 聽 聽 "fmt"

聽 聽 聽 聽 "runtime"

聽 聽 聽 聽 "time"

)


func setValue(ch chan int) {

聽 聽 聽 聽 //ch <- 1 聽让ch1、ch2产生死锁

}


func main() {

runtime.GOMAXPROCS(runtime.NumCPU())

var timeout chan bool = make(chan bool) 聽// 创建一个超时的Channel

go func() { 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽// 新创建一个goroutine

time.Sleep(time.Second * 10) 聽 聽 聽 聽 聽 聽 聽// 休息10秒

timeout <- true 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 // 在10秒内ch1、ch2若还没有向里面写入数据,则认为超时

}() 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 // 加一个()的意思是让这个gorouinte执行


var ch1, ch2 chan int = make(chan int), make(chan int)

go setValue(ch1)

go setValue(ch2)


select {

case <-ch1:

fmt.Println("ch1 ok")

case <-ch2:

fmt.Println("ch2 ok")

case <-timeout: 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽// 若ch1、ch2死锁,10秒钟后timeout填充数据,避免死锁

fmt.Println("Timeout coming...")


}

}

运行一下程序:

wKioL1XR8WiRTShTAACVLbV5LVU524.jpg


三、有意思的程序

啥话都别说了,直接上代码:

package main


import (

聽 聽 聽 聽 "fmt"

)


func main() {

聽 聽 聽 聽 var ch chan int = make(chan int, 1)


聽 聽 聽 聽 for {

聽 聽 聽 聽 聽 聽 聽 聽 select {

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 case ch <- 0:

聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 case ch <- 1:

聽 聽 聽 聽 聽 聽 聽 聽 }


聽 聽 聽 聽 聽 聽 聽 聽 fmt.Printf("%d", <-ch)

聽 聽 聽 聽 }

}

看懂了没有?

解释一下:

1、先定义一个类型为int的Channel

2、再来一个死循环

3、使用select关键字进行选择

4、case的后面是分别向ch写入0或者1

5、使用Printf进行打印结果

在命令行窗口或者git中执行一下该程序,结果如下:

wKiom1XR8lPgtZLbAAZCr9Pecis232.jpg

打印出一连串的随机数


这是为什么呢?

select {

聽 聽 聽 聽 case ch <- 0:

聽 聽 聽 聽 case ch <- 1:

}

这句话的意思就是向channel中放置数据为0或者1的随机数.

本文出自 “青客” 博客,请务必保留此出处http://qingkechina.blog.51cto.com/5552198/1685383


有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:qingkechina

查看原文:【Go语言】【18】GO语言的select

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1021 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传