Go中的Channel

作者: Kenshinsyrup | 来源:发表于2017-02-02 02:44 被阅读557次

    复习下Golang中的Channel学习使我快乐

    译自Channels in Go,该文章实际分两部分,下一部分是Channels in Go - range and select,译文为Go中的Channel——range和select,同样在我的Golang文集中

    goroutine允许我们并行的运行一些代码。但是要想让这些代码对我们来说更有意义,我们会有一些额外的需求--我们应该能够传递数据到正在运行的进程中;当并行的进程成功产生数据时,我们应该能从该进程中获取到数据。channel配合goroutine提供了实现这些需求的途径

    channel可以想象为一个指定了大小和容量的管道或传送带。我们可以在其一边放置内容,然后在另一边获取内容


    图示channel.png

    我们采用一个蛋糕制作装箱工厂的例子来进行下面的说明。我们有一台用于制作蛋糕的机器,还有一台用于装箱的机器。她们通过一条传送带互相连接--蛋糕机将蛋糕放上传送带,装箱机在发现传送带上么有蛋糕时将其取走并装入箱子

    在go中,chan关键字用于定义一个channel。make关键字用于创建cahnnel,创建时指定channel传递的数据类型

    示例代码1
    ic := make(chan int) //a channel that can send and receive an int
    sc := make(chan string) //a channel hat can send and receive a string
    myc := make (chan my_type) //a channel for a custom defined struct type
    

    在channel的变量名前面或后面,你可以使用<-操作符来指示channel用于发送还是接收数据(注意对应关系).假设,my_channel是一个接收int类型数据的channel,你可以像my_channel <- 5这样向其发送数据,并且你可以像my_recvd_value <- my_channel这样来从中接收收据

    想象channel是一个有方向的传送带:
    从外部指向channel的箭头用于向channel放置数据
    从channel指向外部的箭头用于从channel获取数据

    示例代码2
    my_channel := make(chan int)
    
    //within some goroutine - to put a value on the channel
    my_channel <- 5 
    
    //within some other goroutine - to take a value off the channel
    var my_recvd_value int
    my_recvd_value = <- my_channel
    

    当然,你也可以指定channel中的数据移动方向,只需要在创建channel时在chan关键字旁使用<-指明方向

    示例代码3
    ic_send_only := make (<-chan int) //a channel that can only send data - arrow going out is sending
    ic_recv_only := make (chan<- int) //a channel that can only receive a data - arrow going in is receiving 
    

    channel能够保有的数据个数很重要。她能指示具体有多少条数据可以同时工作。即使发送者有能力产生很多条目,如果接受者没有能力接收她们,那么她们就不能工作。这将会有很多蛋糕从传送带上掉落并浪费掉ORZ。在并行计算中,这叫做生产者-消费者同步问题(producer-consumer synchronization problem)

    如果channel的容量(capacity)是1——也就是说,一旦有数据被放入channel,那么该数据必须被取走才能让另一条数据放入,这就是同步channel(synchronous channel)。channel的每一边——发送者和接受者——在同一时间只交流一条数据,然后必须等待,直到另一边完成了相应的发送或接收动作

    目前为止,我们定义的所有的channel默认都是同步channel,也就是说,一条数据被放入channel后必须被取走才能再放置另一条数据。现在,我们完成上面提到的蛋糕制作装箱工厂。由于channel在不同的goroutine之间交流数据,我们有两个名为makeCakeAndSendreceiveCakeAndPack的函数。每个函数都接收一个channel的引用作为参数,这样它们可以通过该channel进行交流

    示例代码4
    package main
    
    import (
        "fmt"
        "time"
        "strconv"
    )
    
    var i int
    
    func makeCakeAndSend(cs chan string) {
        i = i + 1
        cakeName := "Strawberry Cake " + strconv.Itoa(i)
        fmt.Println("Making a cake and sending ...", cakeName)
        cs <- cakeName //send a strawberry cake
    }
    
    func receiveCakeAndPack(cs chan string) {
        s := <-cs //get whatever cake is on the channel
        fmt.Println("Packing received cake: ", s)
    }
    
    func main() {
        cs := make(chan string)
        for i := 0; i<3; i++ {
            go makeCakeAndSend(cs)
            go receiveCakeAndPack(cs)
    
            //sleep for a while so that the program doesn’t exit immediately and output is clear for illustration
            time.Sleep(1 * 1e9)
        }
    }
    
    输出结果
    Making a cake and sending ... Strawberry Cake 1 
    Packing received cake: Strawberry Cake 1 
    Making a cake and sending ... Strawberry Cake 2 
    Packing received cake: Strawberry Cake 2 
    Making a cake and sending ... Strawberry Cake 3 
    Packing received cake: Strawberry Cake 3
    

    在上述代码中,我们创建了三个制作蛋糕的函数调用,并在其之后立刻创建了三个装箱蛋糕的函数调用。我们知道,每当一个蛋糕被装箱,就会有另一个蛋糕同时被制作并准备好被装箱。当然如果你吹毛求疵,代码中确实有一个很轻微的含混之处——在打印Making a cake and sending …和实际发送蛋糕到channel之间有延时。代码中我们在每个循环中调用了time.Sleep(),用于让制作和装箱动作一个接一个的发生,这样做是正确的。由于我们的channel是同步的,而且同一时间仅支持一条数据,一个从channel中移除蛋糕的动作也就是一个装箱动作,必须在制作新蛋糕并将其放入channel之前发生

    现在我们改动下上面的内容让其更像我们正常使用的代码。典型的goroutine一般是一个包含了不断循环的内容的代码块,其内部完成一些操作并且与其他的goroutine通过channel交换数据。在下面的例子中,我们将循环移至goroutine函数内部,然后我们仅调用该goroutine一次

    示例代码5
    package main                                                                                                                                                           
    
    import (
        "fmt"
        "time"
        "strconv"
    )
    
    func makeCakeAndSend(cs chan string) {
        for i := 1; i<=3; i++ {
            cakeName := "Strawberry Cake " + strconv.Itoa(i)
            fmt.Println("Making a cake and sending ...", cakeName)
            cs <- cakeName //send a strawberry cake
        }   
    }
    
    func receiveCakeAndPack(cs chan string) {
        for i := 1; i<=3; i++ {
            s := <-cs //get whatever cake is on the channel
            fmt.Println("Packing received cake: ", s)
        }   
    }
    
    func main() {
        cs := make(chan string)
        go makeCakeAndSend(cs)
        go receiveCakeAndPack(cs)
    
        //sleep for a while so that the program doesn’t exit immediately
        time.Sleep(4 * 1e9)
    }
    
    输出结果
    Making a cake and sending ... Strawberry Cake 1 
    Making a cake and sending ... Strawberry Cake 2 
    Packing received cake: Strawberry Cake 1 
    Packing received cake: Strawberry Cake 2 
    Making a cake and sending ... Strawberry Cake 3 
    Packing received cake: Strawberry Cake 3
    

    输出结果在我电脑上是这样的,在你电脑上可能会不同,输出结果依赖于你机器上goroutine的执行顺序。如前所述,我们仅调用了每个goroutine一次,并且传递了一个公有的channel给她们。在每个goroutine内部有三个循环,makeCakeAndSend将蛋糕放入channel,receiveCakeAndPack将蛋糕从channel中取出。由于程序会在我们创建了两个goroutine后立即结束,因此我们必须手动增加一个时间暂停操作来让三个蛋糕都被制作和装箱好

    极其重要的一点是,我们必须理解,上面的输出并没有正确的反应channel中实际的发送和接收操作。发送和接收在这里是同步的——同一时间仅有一个蛋糕。然而由于在打印语句和实际发送与接收间的延时,输出看起来在顺序上是错误的。而实际上发生的是:

    Making a cake and sending ... Strawberry Cake 1 
    Packing received cake: Strawberry Cake 1 
    Making a cake and sending ... Strawberry Cake 2 
    Packing received cake: Strawberry Cake 2 
    Making a cake and sending ... Strawberry Cake 3 Packing received cake: Strawberry Cake 3
    

    因此,一定要记住,在处理goroutine和channel时,通过打印日志分析执行顺序一定要万分小心

    相关文章

      网友评论

      本文标题:Go中的Channel

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