美文网首页Go语言用例Go知识库
golang语言异步通信之Channel

golang语言异步通信之Channel

作者: CodingCode | 来源:发表于2018-02-07 14:08 被阅读5次

    golang语言异步通信之Channel

    简介

    Channel主要用在go routine之间作为异步通信工具。
    简单说我们可以把Channel理解为一个队列,有人负责往里面写,有人负责从里面读,Channel会保证读和写操作的时序性和原子性,先写入的数据一定先读出来,而且一次读写都是一个完整的数据类型。

    基本用法

    ch <- val    // 把值val写入Channel ch
    val := <-ch  // 从Channel ch中读取值,并且写入变量val
    

    操作符号是<-,箭头向指明数据的流向,注意没有相对的->操作符,

    创建Channel

    ch := make(chan int)
    或者
    var ch chan int
    ch = make(chan int, 1)
    

    和map、slice一样,chan必须先make出来然后才能使用。

    注意make的第二个参数,用来表明chan的缓冲大小,表明允许写入的数据的个数,在缓冲没有满之前,写操作直接返回,而一旦缓冲满了,则写操作会被阻塞,只到有新的缓冲空间释放出来。
    而如果大小为0,则表示没有缓冲,则任何写入操作都会阻塞,知道有读请求进来。

    关闭 Channel

    close(ch)
    

    一个Channel一旦被关闭了,就不再允许继续往里面写入数据,否则会引发panic错误

    panic: send on closed channel
    

    注意,panic只针对写操作,而对于读操作不会panic,而是会立即返回:

    1. 如果Channel里面还有数据,则正常返回读取的数据,就像Channel没有关闭一样。
    2. 如果Channel里面已经没有数据,则返回对应数据类型的零值。如果是int则返回0,如果是string则返回""

    另外可以使用一个额外的参数来检查channel是否已经被关闭了(以示区别读取的是一个真的零值,还是channel已经关闭返回的零值)。

    v, ok := <- ch
    

    如果ok 是false,表明这个channely已经被关闭了。

    Range处理Channel

    for i := range ch {
        fmt.Println(i)
    }
    

    这个for循环会一直迭代,直到channel被关闭;如果ch里面已经没有数据了,for循环会阻塞,只到新的数据进来。

    select操作

    select语句语法类似switch,但只能用来处理Channel相关的操作。
    它的分支(case)可以是Channel的读语句,也可以是Channel的写语句,或者是default语句。

    1. 对于读case语句,如果当前Channel有数据可读,则执行。
    2. 对于写case语句,如果当前Channel有缓冲可写,则执行。
    3. 如果既没有可读,又没有可写,则执行default语句,如果没有default语句,则阻塞select语句。
    • 例子 1
      这个例子走写的分支,即会打印"put value to chan",因为ch没有数据第一个case读操作不满足,第二个case写可以写入,因为Channel有一个缓冲大小。
    func main() {
        ch := make(chan int, 1)
        select {
            case i := <- ch:
                fmt.Printf("get value from chan: %d\n", i)
            case ch <- 1:
                fmt.Printf("put value to chan\n")
        }
    }
    
    • 例子 2
      这个例子会阻塞,因为没有数据读,而又没有缓冲,写也不能成功。
    func main() {
        ch := make(chan int)
        select {
            case i := <- ch:
                fmt.Printf("get value from chan: %d\n", i)
            case ch <- 1:
                fmt.Printf("put value to chan\n")
        }
    }
    
    • 例子 3:
      这个例子会走到default分支,打印出"select default branch",因为读和写的分支都不满足,又有default分支。
    func main() {
        ch := make(chan int)
        select {
            case i := <- ch:
                fmt.Printf("get value from chan: %d\n", i)
            case ch <- 12:
                fmt.Printf("put value to chan\n")
            default:
                fmt.Printf("select default branch\n")
        }
    }
    

    select的Timeout

    前面我们看到如果select语句没有分支(case)语句能够处理,而又当没有定义default分支时,select会一直阻塞,这时候就需要一个超时机制。

    func main() {
        ch := make(chan int, 1)
        select {
            case i := <- ch:
                fmt.Printf("get value from chan: %d\n", i)
            case <-time.After(time.Second * 2):
                fmt.Println("timeout 2 seconds")
        }
    }
    

    这个例子里,两秒之后超时会触发,打印"timeout 2 seconds"。
    这个超时机制听起来很高级,其实它就是利用的是time.After方法,我们看下time.After的定义:

    # time/sleep.go
    func After(d Duration) <-chan Time {
        return NewTimer(d).C
    }
    ...
    func NewTimer(d Duration) *Timer {
        c := make(chan Time, 1)
        t := &Timer{
            C: c,
            r: runtimeTimer{
                when: when(d),
                f:    sendTime,
                arg:  c,
            },
        }
        startTimer(&t.r)
        return t
    }
    

    我们看到time.After()返回的是一个类型为<-chan Time的单向的channel,这个channel里面有一个值,是一个时间戳值,从而保证在时间到期之后返回的channel里面有一个值,那么这个分支的读取操作能够满足不会阻塞。

    想到另外一个场景,select的各个分支都是函数,函数返回一个Channel类型值。

    func foo1(ch chan int) chan int {
        fmt.Printf("in foo1\n")
        return ch
    }
    
    func foo2(ch chan int) chan int {
        fmt.Printf("in foo2\n")
        return ch
    }
    
    func main() {
        ch1 := make(chan int, 1)
        ch2 := make(chan int, 1)
    
        ch1 <- 11
        ch2 <- 22
    
        select {
            case i := <- foo1(ch1):
                fmt.Printf("get value from chan1: %d\n", i)
            case i := <- foo2(ch2):
                fmt.Printf("get value from chan2: %d\n", i)
        }
    }
    

    试猜想执行输出结果是什么?是下面这个吗?

    in foo1
    get value from chan1: 11
    

    很多人第一直觉应该是的;我们运行一下看看结果:

    $ go build && ./main 
    in foo1
    in foo2
    get value from chan1: 11
    $ go build && ./main 
    in foo1
    in foo2
    get value from chan1: 11
    $ go build && ./main 
    in foo1
    in foo2
    get value from chan2: 22
    

    上面运行了三次,注意两点:

    1. select的两个分支任何一个都可能被执行到,他们是随机的。即当select的多个分支都满足条件时,哪一个会被选中执行是随机的,不是严格按照代码顺序。
    2. 不管执行了哪一个select分支,函数foo1()和函数foo2()都被执行了。这个怎么理解呢,因为select语句关注的是channel对象,只有执行了函数foo1()和foo2()才能从返回值中拿到Channel对象,从而才能知道Channel是否能够满足读或者写的需求,即只有找到各个select分支的Channel对象本身,才能判断Channel的状态。

    相关文章

      网友评论

        本文标题:golang语言异步通信之Channel

        本文链接:https://www.haomeiwen.com/subject/agvozxtx.html