这是我参与8月更文挑战的第 24 天,活动详情查看: 8月更文挑战
select
假设现在有两个通道c1和c2,我想同时收这两个通道中的值,谁先有值,我就先取哪个通道里的值。这个就需要用到select
package main
import "fmt"
func main() {
var c1, c2 chan int // c1 and c2 is nil
select {
case n := <-c1:
fmt.Println("Receiver from c1: ", n)
case n := <- c2:
fmt.Println("Receiver from c2: ", n)
default:
fmt.Println("No value Received")
}
}
因为c1和c2未初始化,所以都是nil,但是此时select也能正常运行,只是去不到值,所以执行default部分。前边的文章有说到,channel的发送数据或接收数据都是阻塞式的,而这里这就好像做了一个非阻塞式的获取
select的使用
下边通过示例来了解select的使用
package main
import (
"fmt"
"math/rand"
"time"
)
func generator() chan int {
out := make(chan int)
go func() {
i := 0
for {
time.Sleep(
time.Duration(rand.Intn(1500)) *
time.Millisecond)
out <- i
i++
}
}()
return out
}
func main() {
var c1, c2 = generator(), generator()
for {
select {
case n := <-c1:
fmt.Println("Receiver from c1: ", n)
case n := <- c2:
fmt.Println("Receiver from c2: ", n)
}
}
}
输出结果:
Receiver from c1: 0
Receiver from c2: 0
Receiver from c2: 1
Receiver from c1: 1
Receiver from c1: 2
Receiver from c2: 2
Receiver from c1: 3
Receiver from c2: 3
Receiver from c2: 4
Receiver from c2: 5
Receiver from c1: 4
Receiver from c1: 5
Receiver from c1: 6
Receiver from c2: 6
.......
可以发现c1和c2输出数据的速度不一样,谁先有数据,select就会先获取哪个通道的数据,如果是两个同时有,它就会随机选一个
现在对上边的代码做如下修改,增加一个worker函数,它用于打印channel中的值。再增加一个createWorker方法,用于创建一个通道,并且开一个goroutine去打印通道中的值。有了这两个函数,在select的时候,当某个通道中有值的时候,就不用我们手动用Println的方式打印了,而是通过createWorker再创建一个channel,然后将c1或c2中取到的值,给这个创建出来的channel,由worker去输出结果。具体如下:
package main
import (
"fmt"
"math/rand"
"time"
)
func generator() chan int {
out := make(chan int)
go func() {
i := 0
for {
time.Sleep(
time.Duration(rand.Intn(1500)) *
time.Millisecond)
out <- i
i++
}
}()
return out
}
func worker(id int, c chan int) {
for n := range c {
fmt.Printf("worker %d, received %d\n", id, n)
}
}
func createWorker(id int) chan<- int {
c := make(chan int)
go worker(id, c)
return c
}
func main() {
var c1, c2 = generator(), generator()
w := createWorker(0) //创建一个channel,用于接收c1或c2中的值
for {
select {
case n := <-c1:
w <- n
case n := <- c2:
w <- n
}
}
}
说明:对于一个是nil的channel,如果select中没有default,那它会一直阻塞住
上边这种做法会有一个缺点,就是select中收到一个数之后(c1或c2),后边执行的w <- n又会是阻塞的(因为通道的发送和接收是阻塞式的)。所以,这样并不好,我们可以在select中再加一种case的情况,当从c1或c2中取到值之后,发到w中。因此,我们是需要知道是否从c1或c2中获取到值,通过一个变量hasValue来标记是否获取到值
对main函数做如下修改即可
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
n := 0
hasValue := false//标记是否从c1或c2中获取到值
for {
var activeWorker chan<- int//因为worker是一个值发送数据的channel
if hasValue {
activeWorker = worker
}
select {
case n = <-c1:
hasValue = true
case n = <- c2:
hasValue = true
case activeWorker <- n:
hasValue = false
}
}
}
上边这个程序其实还有问题,但是不太容易出现,因为activeWorker中的数据从c1、c2中来,那么activeWorker消耗数据的速度可能和c1、c2生成数据的速度是不一样的。如果生成数据的速度太快了,比如一口气生成了1、2、3三个数据,全都给了n,n就会连续的去收,最后这个n就是3,那1和2就输出不了了(可以mock一下这种情况,让worker在打印的时候,时间长一点,sleep五秒)
func worker(id int, c chan int) {
for n := range c {
time.Sleep(3 * time.Second)
fmt.Printf("worker %d, received %d\n", id, n)
}
}
输出:
worker 0, received 0
worker 0, received 5
worker 0, received 9
worker 0, received 11
......
可以发现有些数据就会跳掉,没有输出出来
因此我们的处理办法就是,将所有的n存下来排队,负责消费处理的channel可以一个一个处理。所以修改后的代码如下:
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
var values []int
for {
var activeWorker chan<- int//因为worker是一个值发送数据的channel
var activeValue int
if len(values) > 0 { // 只要slice中还有值,就取出处理
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <- c2:
values = append(values, n)
case activeWorker <- activeValue:
values = values[1:]
}
}
}
输出:
......
worker 0, received 2
worker 0, received 2
worker 0, received 3
worker 0, received 3
worker 0, received 4
worker 0, received 5
worker 0, received 4
worker 0, received 5
worker 0, received 6
worker 0, received 6
......
此时发现,就算worker打印的很慢,也不会有数据丢失(我为了展示出来打印结果会是乱序的,所以省略了打印结果的前后部分)
定时器的使用
这个过程中,values中肯定积压了很多数据,我们也可以看里边积压了多少数据。现在的程序会一直打印数据,结束不了。假设我们想让它打印10s之后退出,需要用到一个定时器time.After(10 * time.Second),它返回的是一个channel,等10s之后,它会向这个channel中发送一个时间,这样就可以在select中去接收,收到之后就退出,修改如下:
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
var values []int
tm := time.After(10 * time.Second) //这个方法返回的是一个channel。也就是说他会在10s之后,向这个channel中发送一个时间
for {
var activeWorker chan<- int//因为worker是一个值发送数据的channel
var activeValue int
if len(values) > 0 { // 只要slice中还有值,就取出处理
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <- c2:
values = append(values, n)
case activeWorker <- activeValue:
values = values[1:]
case <- tm:
fmt.Println("Bye")
return
}
}
}
假设我们认为,如果一个数据超过800ms还没有生成出来,就认为超时,也可以使用上边的time.After方法
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
var values []int
tm := time.After(10 * time.Second) //这个方法返回的是一个channel。也就是说他会在10s之后,向这个channel中发送一个时间
for {
var activeWorker chan<- int//因为worker是一个值发送数据的channel
var activeValue int
if len(values) > 0 { // 只要slice中还有值,就取出处理
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <- c2:
values = append(values, n)
case activeWorker <- activeValue:
values = values[1:]
case <-tm: //这个是总的时间,从一开始到当前,一共是10s
fmt.Println("Bye")
return
case <-time.After(800*time.Millisecond)://每两次,如果生成数据的时间差大于800ms,就会触发
fmt.Println("timeout")
}
}
}
如果担心slice中积压的数据太多,可以增加一个定时的功能,每秒钟看一下积压的数据。可以利用time.Tick方法,它返回的也是一个channel,每隔指定的时间段,会往队列中发送一条数据,此时我们也可以在case中接收
func main() {
var c1, c2 = generator(), generator()
var worker = createWorker(0)
var values []int
tm := time.After(10 * time.Second) //这个方法返回的是一个channel。也就是说他会在10s之后,向这个channel中发送一个时间
tick := time.Tick(time.Second)//每秒钟往返回的channel中发送一条数据
for {
var activeWorker chan<- int//因为worker是一个值发送数据的channel
var activeValue int
if len(values) > 0 { // 只要slice中还有值,就取出处理
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <- c2:
values = append(values, n)
case activeWorker <- activeValue:
values = values[1:]
case <-tm: //这个是总的时间,从一开始到当前,一共是10s
fmt.Println("Bye")
return
case <-time.After(800*time.Millisecond)://每两次,如果生成数据的时间差大于800ms,就会触发
fmt.Println("timeout")
case <-tick:
fmt.Println("queue len: ", len(values))
}
}
}