Golang, 以17个简短代码片段,切底弄懂 channel 基础

网上看到一篇文章,总结的很不错,在此整理记录。

基本概念

关于管道 Channel

  1. Channels用来同步并发执行的函数并提供它们某种传值交流的机制。
  2. Channels的一些特性:通过channel传递的元素类型、容器(或缓冲区)和传递的方向由“<-”操作符指定。
  3. c<-123,把值123输入到管道 c,<-c,把管道 c 的值读取到左边,value :=<-c,这样就是读到 value里面。

管道分类

无缓冲的与有缓冲channel有着重大差别,那就是一个是同步的 一个是非同步的。

比如:

c1:=make(chan int)         无缓冲
c2:=make(chan int,1)      有缓冲

    例如:c1<-1

例子

演示 无缓存 和 有缓冲 的 channel 的样子

func test0(){
    /** 演示 无缓存 和 有缓冲 的 channel 的样子 */
    done  := make(chan bool)   /** 无缓冲 */
    done1 := make(chan bool,1) /** 有缓冲 */
    println(done,done1)
}

演示 无缓冲在同一个main里面的 死锁例子

func test1()  {
    /** 编译错误 deadlock,阻死 main 进程 */
    /** 演示 无缓冲在同一个main里面的 死锁例子 */
    done := make(chan bool)
    done<-true      /** 这句是输入值,它会一直阻塞,等待读取 */
    <-done          /** 这句是读取,但是在上面已经阻死了,永远走不到这里 */
    println("完成")
}

演示仅有 输入 语句,但没 读取语句 的死锁例子

func test2()  {
    /** 编译错误 deadlock,阻死 main 进程 */
    /** 演示仅有 输入 语句,但没 读取语句 的死锁例子 */
    done := make(chan bool)
    done<-true  /** 输入,一直等待读取,哪怕没读取语句 */
    println("完成")
}

演示仅有 读取 语句,但没 输入语句 的死锁例子

func test3()  {
    /** 编译错误 deadlock,阻死 main 进程 */
    /** 演示仅有 读取 语句,但没 输入语句 的死锁例子 */
    done := make(chan bool)
    <-done    /** 读取输出,前面没有输入语句,done 是 empty 的,所以一直等待输入 */

    println("完成")
}

演示,协程的阻死,不会影响 main

func test4()  {
    /** 编译通过 */
    /** 演示,协程的阻死,不会影响 main */
    done := make(chan bool)
    go func() {
        <-done /** 一直等待 */
    }()
    println("完成")
    /**
     * 控制台输出:
     *       完成
     */
}

在 test4 的基础上,无缓冲channel在协程 go routine 里面阻塞死

func test5()  {
    /** 编译通过 */
    /** 在 test4 的基础上,无缓冲channel在协程 go routine 里面阻塞死 */
    done := make(chan bool)
    go func() {
        println("我可能会输出哦") /** 阻塞前的语句 */
        done<-true  /** 这里阻塞死,但是上面那句有可能输出,见 test3 的结论 */
        println("我永远不会输出")
        <-done      /** 这句也不会走到,除非在别的协程里面读取,或者在 main */
    }()
    println("完成")
}

编译通过,在 test5 的基础上演示,延时 main 的跑完

func test6()  {
    /** 编译通过,在 test5 的基础上演示,延时 main 的跑完 */
    done := make(chan bool)
    go func() {
        println("我可能会输出哦")
        done<-true  /** 这里阻塞死 */
        println("我永远不会输出")
        <-done      /** 这句也不会走到 */
    }()
    time.Sleep(time.Second * 1)  /** 加入延时 1 秒 */
    println("完成")
    /**
     * 控制台输出:
     *       我可能会输出哦
     *       完成
     */
    /**
     * 结论:
     *    如果在 go routine 中阻塞死,也可能不会把阻塞语句前的内容输出,
     *    因为main已经跑完了,所以延时一会,等待 go routine
     */
}

演示无缓冲channel 在 不同的位置里面接收填充和接收

func test7()  {
    /** 编译通过,演示无缓冲channel 在 不同的位置里面接收填充和接收*/
    done := make(chan bool)
    go func() {
        done<-true  /** 直到,<-done 执行,否则这里阻塞死 */
        println("我永远不会输出,除非 <-done 执行")

    }()
    <-done      /** 这里接收,在输出完成之前,那么上面的语句将会走通 */
    println("完成")
    /**
     * 控制台输出:
     *       我永远不会输出,除非 <-done 执行
     *       完成
     */
}

演示无缓冲channel 在不同地方接收的影响

func test8()  {
    /** 编译通过,演示无缓冲channel 在不同地方接收的影响 */
    done := make(chan bool)
    go func() {
        done<-true  /** 直到,<-done 执行,否则这里阻塞死 */
        println("我永远不会输出,除非 <-done 执行")
    }()
    println("完成")
    <-done      /** 这里接收,在输出完成之后 */
    /**
     * 控制台输出:
     *       完成
     *       我永远不会输出,除非 <-done 执行
     */
}

没缓存的 channel 使用 close 后,不会阻塞

func test9()  {
    /** 编译通过 */
    /** 演示,没缓存的 channel 使用 close 后,不会阻塞 */
    done := make(chan bool)
    close(done)
    //done<-true  /** 关闭了的,不能再往里面输入值 */
    <-done        /** 这句是读取,但是在上面已经关闭 channel 了,不会阻死 */
    println("完成")
}

没缓存的 channel,在 go routine 里面使用 close 后,不会阻塞

func test10()  {
    /** 编译通过 */
    /** 演示,没缓存的 channel,在 go routine 里面使用 close 后,不会阻塞 */
    done := make(chan bool)
    go func() {
        close(done)
    }()
    //done<-true  /** 关闭了的,不能再往里面输入值 */
    <-done        /** 这句是读取,但是在上面已经关闭 channel 了,不会阻死 */
    println("完成")
}

有缓冲的 channel 不会阻塞的例子

func test11()  {
    /** 编译通过 */
    /** 有缓冲的 channel 不会阻塞的例子 */
    done := make(chan bool,1)
    done<-true
    <-done
    println("完成")
}

有缓冲的 channel 会阻塞的例子

func test12()  {
    /** 编译通过 */
    /** 有缓冲的 channel 会阻塞的例子 */
    done := make(chan bool,1)
    // done<-true /** 注释这句 */
    <-done /** 虽然是有缓冲的,但是在没输入的情况下,读取,会阻塞 */
    println("完成")
}

有缓冲的 channel 会阻塞的例子

func test13()  {
    /** 编译不通过 */
    /** 有缓冲的 channel 会阻塞的例子 */
    done := make(chan bool,1)
    done<-true
    done<-false /** 放第二个值的时候,第一个还没被人拿走,这时候才会阻塞,根据缓冲值而定 */
    println("完成")
}

有缓冲的 channel 不会阻塞的例子

func test14()  {
    /** 编译通过 */
    /** 有缓冲的 channel 不会阻塞的例子 */
    done := make(chan bool,1)
    done<-true   /** 不会阻塞在这里,等待读取 */

    println("完成")
}

有缓冲的channel,如果在 go routine 中使用,一定要做适当的延时,否则会输出来不及,因为main已经跑完了,所以延时一会,等待 go routine

func test15()  {
    /** 编译通过 */
    /** 有缓冲的channel 在 go routine 里面的例子 */
    done := make(chan bool,1)
    go func() {
        /** 不会阻塞 */
        println("我可能会输出哦")
        done<-true  /** 如果把这个注释,也会导致 <-done 阻塞 */
        println("我也可能会输出哦")
        <-done
        println("别注释 done<-true 哦,不然我就输出不了了")
    }()
    time.Sleep(time.Second * 1)  /** 1秒延时,去掉就可能上面的都不会输出也有可以输出,routine 调度 */
    println("完成")
    /**
     * 控制台输出:
     *       我可能会输出哦
     *       我也可能会输出哦
     *       完成
     */
    /**
     * 结论:
     *    有缓冲的channel,如果在 go routine 中使用,一定要做适当的延时,否则会输出来不及,
     *    因为main已经跑完了,所以延时一会,等待 go routine
     */
}

多channel模式

func getMessagesChannel(msg string, delay time.Duration) <-chan string {
    c := make(chan string)
    go func() {
        for i := 1; i <= 3; i++ {
            c <- fmt.Sprintf("%s %d", msg, i)
            time.Sleep(time.Millisecond * delay) /** 仅仅起到,下一次的 c 在何时输入 */
        }
    }()
    return c
}

func test16()  {
    /** 编译通过 */
    /** 复杂的演示例子 */
    /** 多channel模式 */
    c1 := getMessagesChannel("第一", 600 )
    c2 := getMessagesChannel("第二", 500 )
    c3 := getMessagesChannel("第三", 5000)

    /** 层层限制阻塞 */
    /** 这个 for 里面会造成等待输入,c1 会阻塞 c2 ,c2 阻塞 c3 */
    /** 所以它总是,先输出 c1 然后是 c2 最后是 c3 */
    for i := 1; i <= 3; i++ {
        /** 每次循环提取一轮,共三轮 */
        println(<-c1)  /** 除非 c1 有输入值,否则就阻塞下面的 c2,c3 */
        println(<-c2)  /** 除非 c2 有输入值,否则就阻塞下面的 c3 */
        println(<-c3)  /** 除非 c3 有输入值,否则就阻塞进入下一轮循环,反复如此 */
    }
    /**
     *  这个程序的运行结果,首轮的,第一,第二,第三 很快输出,因为
     *  getMessagesChannel 函数的延时 在 输入值之后,在第二轮及其之后
     *  因为下一个 c3 要等到 5秒后才能输入,所以会阻塞第二轮循环的开始5秒,如此反复。
     */
    /** 修改:如果把 getMessagesChannel 里面的延时,放在输入值之前,那么 c3 总是等待 5秒 后输出 */
}

在 test15 基础修改的,复杂演示例,多channel 的选择,延时在输入之后的情况

func test17()  {
    /** 编译通过 */
    /** 在 test15 基础修改的,复杂演示例子 */
    /** 多channel 的选择,延时在输入之后的情况 */
    c1 := getMessagesChannel("第一", 600 )
    c2 := getMessagesChannel("第二", 500 )
    c3 := getMessagesChannel("第三", 5000)
    /** 3x3 次循环,是 9 */
    /** select 总是会把最先完成输入的channel输出,而且,互不限制 */
    /** c1,c2,c3 每两个互不限制 */
    for i := 1; i <= 9; i++ {
        select {
        case msg := <-c1:
            println(msg)
        case msg := <-c2:
            println(msg)
        case msg := <-c3:
            println(msg)
        }
    }
    /**
     * 这个程序的运行结果:
     *    第二 1,第三 1,第一 1,第二 2,第一 2,第二 3,第一 3,第三 2,第三 3
     */
    /** 分析:前3次输出,“第一”,“第二”,“第三”,都有,而且
     *  是随机顺序输出,因为协程的调度,第4,5,6次,由于“第二”只延时 500ms,
     *  比 600ms 和 5000ms 都要小,那么它先输出,然后是“第一”,此时“第三”还不能输出,
     *  因为它还在等5秒。此时已经输出5次,再过 500ms,"第三"的5秒还没走完,所以继续输出"第一",
     *  再过 100ms,500+100=600,"第二"也再完成了一次,那么输出。至此,"第一"和"第二"已经
     *  把管道的 3 个值全部输出,9-7 = 2,剩下两个是 "第三"。此时,距离首次的 5000ms 完成,
     *  还有,500-600-600 = 3800ms,达到后,"第三" 将输出,再过5秒,最后一次"第三输出"
     */
}

enjoy!