Go语言并发之Select多路选择操作符用法详解
1、Go语言并发之Select多路选择操作符
select 是类 UNIX 系统提供的一个多路复用系统 API,Go 语言借用多路复用的概念,提供了 select 关键字,用于多路监听多个通道。当监听的通道没有状态是可读或可写的,select 是阻塞的;只要监听的通道中有一个状态是可读或可写,则 select 就不会阻寒,而是进入处理就绪通道的分支流程。如果监听的通道有多个口读或口写的状态,则 select 随利选取一个处理。
package main func main() { ch := make(chan int, 1) go func(chan int) { for { select { // 0或者1写入是随机的 case ch <- 0: case ch <- 1: } } }(ch) for i := 0; i < 10; i++ { println(<-ch) } }
输出
# 程序结果
1
1
1
1
0
0
0
0
1
1
1.1 多路选择操作符 select
在golang语言中,select 语句就是用来监听和channel有关的IO操作,当IO操作发生时,触发相应的case动作。
有了select语句,可以实现 main 主线程与 goroutine 线程之间的互动。
select使用时类似 switch-case 的用法,适用于处理多通道的场景,会通过类似 are-you-ready-polling 的机制来工作。
select { case <-ch1 : // 检测有没有数据可读 // 一旦成功读取到数据,则进行该case处理语句 case ch2 <- 1 : // 检测有没有数据可写 // 一旦成功向ch2写入数据,则进行该case处理语句 default: // 如果以上都没有符合条件,那么进入default处理流程 }
select 语句只能用于 channel 信道的IO操作,每个 case 都必须是一个信道。
如果不设置 default 条件,当没有IO操作发生时,select 语句就会一直阻塞。
如果有一个或多个IO操作发生时,Go运行时会随机选择一个 case 执行,但此时将无法保证执行顺序。
对于 case 语句,如果存在信道值为 nil 的读写操作,则该分支将被忽略,可以理解为相当于从select语句中删除了这个case;
对于空的 select 语句,会引起死锁;
对于在 for中的select语句,不能添加 default,否则会引起cpu占用过高的问题;
随机性:多个 case 之间并非顺序的,遵循「先到先执行,同时到则随机执行」的原则。
一次性:和 switch-case 一样,select-case也只会执行一次,如果需要多次处理,需要在外层套一个循环。
default 不会阻塞,会一直执行,当与 for 循环组合使用时可能出现死循环。
1.2 阻塞与非阻塞 select
select 默认是阻塞的,当没有 case 处于激活状态时,会一直阻塞住,极端的甚至可以这样用:
package main func main() { select { // 啥也不干,一直阻塞住 } }
执行后,引发死锁,打印如下:
# 输出
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
通过增加 default,可以实现非阻塞的 select:
select { case x, ok := <-ch1: ... case ch2 <- y: ... default: fmt.Println("default") }
1.3 多 case 与 default 执行的顺序
整体流程如图所示:
1.4 多个IO操作发生时,case语句是随机执行的
package main import "fmt" func main() { // 创建一个长度带缓冲的整型通道 ch1 := make(chan int, 1) // 向通道中写入数据 ch1 <- 1 ch2 := make(chan int, 1) ch2 <- 2 select { case <-ch1: fmt.Println("ch1 read") case <-ch2: fmt.Println("ch2 read") } }
多次执行后,会随机打印 ch1 read 或 ch2 read。
1.5 for中的select 引起CPU资源消耗过高
package main import ( "fmt" "time" ) func main() { quit := make(chan bool) go func() { for { select { case <-quit: fmt.Println("quit") // 使用 return 就会退出整个goroutine线程;如果使用 break,程序仍然在for循环中执行 return default: fmt.Println("default") } } }() time.Sleep(3 * time.Second) quit <- true // 主线程在3秒后,向quit信道写入数据 time.Sleep(2 * time.Second) fmt.Println("main") }
输出:
# 程序结果
default
default
default
default
default
default
default
......
......
default
default
default
default
default
quit
main
在 for{} 的 select 语句中使用了 default 后,线程就会无限执行 default 条件,直到 quit 信道中读到数据,否则会一直在一个死循环中运行,从而导致占满整个CPU资源。
在 for{} 的 select 语句中,不建议使用 default 条件。
1.6 select语句的实际应用
(1)、实现 main主线程与 goroutine线程之间的交互、通信
package main import ( "bufio" "fmt" "os" ) // 通过控制台输入"bye",来控制main函数结束运行 func main() { quit := make(chan bool) ch := make(chan string) go func() { for { select { case name := <-ch: fmt.Printf("from main msg: [%v]\n", name) if name == "bye" { quit <- true } else { quit <- false } } } }() for { // 控制台输入 fmt.Print("please input string: ") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() ch <- scanner.Text() isOver := <-quit if isOver { break } } fmt.Println("main over") }
输出:
please input string: from main msg: [ttttt]
please input string: from main msg: [qqqq]
please input string: from main msg: [wwww]
please input string: from main msg: [bye]
main over
(2)、超时实现
package main import ( "fmt" "time" ) func main() { quit := make(chan bool) ch := make(chan int) go func() { for { select { case num := <-ch: fmt.Println("num = ", num) case <-time.After(5 * time.Second): fmt.Println("超时") quit <- true } } }() for i := 0; i < 2; i++ { ch <- i time.Sleep(time.Second) } <-quit // 等待超时后, 结束 main主线程 fmt.Println("程序结束") }
输出:
num = 0
num = 1
超时
程序结束
1.7 select使用的区别
package main import ( "fmt" "time" ) func server1(ch chan string) { time.Sleep(6 * time.Second) ch <- "from server1" } func server2(ch chan string) { time.Sleep(3 * time.Second) ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) s1 := <-output1 fmt.Println(s1) s2 := <-output2 fmt.Println(s2) }
程序结果
from server1
from server2
package main import ( "fmt" "time" ) func server1(ch chan string) { time.Sleep(6 * time.Second) ch <- "from server1" } func server2(ch chan string) { time.Sleep(3 * time.Second) ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) } }
程序结果
from server2
package main import "time" import ( "fmt" ) // select 管道参数并行 func server1(ch chan string) { time.Sleep(time.Second * 6) ch <- "response from server1" } func server2(ch chan string) { time.Sleep(time.Second * 3) ch <- "response from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) // 管道同时ready,select随机执行 // time.Sleep(time.Second) select { case s1 := <-output1: fmt.Println("s1:", s1) case s2 := <-output2: fmt.Println("s2:", s2) default: fmt.Println("run default") } }
程序结果
run default
package main import ( "fmt" "time" ) func server1(ch chan string) { ch <- "from server1" } func server2(ch chan string) { ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) time.Sleep(1 * time.Second) select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) } }
输出
from server2 和 from server1 随机交替
以上就是Go语言并发之Select多路选择操作符用法详解的详细内容,更多关于Go Select的资料请关注脚本之家其它相关文章!
相关文章
Golang源码分析之golang/sync之singleflight
golang/sync库拓展了官方自带的sync库,提供了errgroup、semaphore、singleflight及syncmap四个包,本次先分析第一个包errgroup的源代码,下面这篇文章主要给大家介绍了关于Golang源码分析之golang/sync之singleflight的相关资料,需要的朋友可以参考下2022-11-11
最新评论