Go语言的channel选择器:select

1. select是什么

select是Go语言层面提供的一种多路复用机制,用于检测当前goroutine连接的多个channel是否有数据准备完毕,可用于读或写

2. IO多路复用

看到select,很自然的会联想到linux提供的io多路复用模型,selectpollepoll,IO复用主要用于提升程序处理io事件的性能。Go语言中的selectlinux中的select有一定得区别。
操作系统中的IO多路复用简单理解就就是用一个或者是少量线程处理多个IO事件。简单对比一下传统的阻塞IO与IO多路复用
传统阻塞IO:对于每一个网络IO事件,操作系统都会起一个线程去处理,在IO事件没准备好的时候,当前线程就会一直阻塞

select1

  • 优点:逻辑简单,在阻塞等待期间线程会挂起,不会占用 CPU 资源。

  • 缺点:每个连接需要独立的线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大
    IO多路复用的基本原理如下图所示:

    select2

  • 优点:通过复用一个线程处理了多个IO事件,无需对额外过多的线程维护管理,资源和效率上都获得了提升

  • 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低

Go语言的select语句,是用来起一个goroutine监听多个channel的读写事件,提高从多个channel获取信息的效率,相当于也是单线程处理多个IO事件,其思想基本相同

3. select用法

select的基本使用模式如下:

select {
        case <- channel1:     // 如果从channel1读取数据成功,执行case语句 
          do ...   
        case channel2 <- 1:   // 如果向channel2写入数据成功,执行case语句 
          do ...          
        default:              // 如果上面都没有成功,进入default处理流程
          do ...
}

可以看到,select的用法形式类似于switch,但是区别于switch的是,对于select各个case的表达式必须都是channel的读写操作select通过多个case语句监听多个channel的读写操作是否准备好,可以执行,其中任何一个case看可以执行了则选择该case语句执行,如果没有可以执行的case,则执行default语句,如果没有default,则当前goroutine会阻塞

3.1 空select永久阻塞

当一个select中什么语句都没有,没有任何case,将会永久阻塞

package main

func main() {
        select {
        }
}

运行结果

 
fatal error: all goroutines are asleep - deadlock!

程序因为select语句导致永久阻塞,当前goroutine阻塞之后,由于Go语言自带死锁检测机制,发现当前goroutine永远不会被唤醒,会报上述死锁错误

3.2 没有default且case无法执行的select永久阻塞

看下面示例

package main

import (
   "fmt"
)

func main() {
   ch1 := make(chan int, 1)
   ch2 := make(chan int, 1)
   select {
   case <-ch1:
      fmt.Printf("received from ch1")
   case num := <-ch2:
      fmt.Printf("num is: %d", num)
   }
}

运行结果

fatal error: all goroutines are asleep - deadlock!

程序中select从两个channelch1ch2中读取数据,但是两个channel都没有数据,且没有goroutine往里面写数据,所以不可能读到数据,这两个case永远无法执行到,select也没有default,所以会出现永久阻塞,报死锁

3.3 有单一case和default的select

package main

import (
   "fmt"
)

func main() {
   ch := make(chan int, 1)
   select {
   case <-ch:
      fmt.Println("received from ch")
   default:
      fmt.Println("default!!!")
   }
}

运行结果:

default!!!

执行到select语句的时候,由于ch中没有数据,且没有goroutinechannel中写数据,所以不可能执行到,就会执行default语句,打印出default!!!

3.4 有多个case和default的select

package main

import (
   "fmt"
   "time"
)

func main() {
   ch1 := make(chan int, 1)
   ch2 := make(chan int, 1)
   go func() {
      time.Sleep(time.Second)
      for i := 0; i < 3; i++ {
         select {
         case v := <-ch1:
            fmt.Printf("Received from ch1, val = %d\n", v)
         case v := <-ch2:
            fmt.Printf("Received from ch2, val = %d\n", v)
         default:
            fmt.Println("default!!!")
         }
         time.Sleep(time.Second)
      }
   }()
   ch1 <- 1
   time.Sleep(time.Second)
   ch2 <- 2
   time.Sleep(4 * time.Second)
}

运行结果:

Received from ch1, val = 1
Received from ch2, val = 2
default!!!

goroutine中向后往管道ch1ch2中发送数据,在子goroutine中执行两个select,可以看到,在执行select的时候,那个case准备好了就会执行当下case的语句,最后没有数据可接受了,没有case可以执行,则执行default语句。
这里注意:当多个case都准备好了的时候,会随机选择一个执行

package main

import (
    "fmt"
)

func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    ch1 <- 5
    ch2 <- 6
    select {
    case v := <-ch1:
       fmt.Printf("Received from ch1, val = %d\n", v)
    case v := <-ch2:
       fmt.Printf("Received from ch2, val = %d\n", v)
    default:
       fmt.Println("default!!!")
    }
}

运行结果:

Received from ch2, val = 6

多次执行,2个case都有可能打印,这就是select选择的随机性

交流学习

如果您觉得文章有帮助,请帮忙转发给更多好友,或关注公众号:IT杨秀才,持续更新更多硬核文章,一起聊聊互联网网那些事儿!

公众号二维码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值