GO语言基础篇(二十四)- select多路复用

1,296 阅读1分钟

这是我参与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))
        }
    }
}