7.2 什么是Go语言中的管道Channel

7.2 什么是Go语言中的管道Channel什么是管道Channel为了解决与Groutines间的通讯问题,Go中提供给了管道Channel。Channel有点像是Linux系统的双向通

大家好,欢迎来到IT知识分享网。7.2

什么是管道Channel

为了解决与Groutines间的通讯问题,Go中提供给了管道Channel。Channel有点像是Linux系统的双向通讯管道,既可以发送消息,也可以接受消息。管道需要明确处理的数据类型,也就是在声明管道时必须还要声明类型。管道的定义方法如下:

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

管道的使用

创建

我们建立这样的一个管道,这是一个没有任何缓存的bool型管道

ch := make(chan bool)

也可以通过指定缓冲区大小定义管道的长度,下面示例中缓冲区可以存放两个元素,如果超过2个元素,则会阻塞并等待取走管道后,再进行写入,后续会有示例来详细讲解

ch := make(chan bool, 2)

读取

这是读取管道的方法,程序运行时,将产生阻塞,直到从管道内读取到值

value := <- ch

读取时,管道内将返回两个值,其中第二个值可以作为channel是否关闭的判断条件,以帮助我们更好的控制并发

value, open := <-ch
if !open {
    fmt.Println("Channel is already closed")
}

写入

这是写入管道的方法,程序运行时,也将阻塞,一直等待有人将值读取走

ch <- true

单并发使用示例

我们尝试来解决上一节遗留的问题,我们通过Channel在主函数中等待Channel实现异步的控制。
我们首先声明了一个channel,用于传输整数

ch := make(chan int)

在ready函数中,我们在函数最后将运行时间输入管道之中

c <- s

而在主函数中,我们读取管道内返回的值,这里管道其实并没有关闭,所以ok中返回的值仍然为true,管道仍然是打开状态

value, ok := <- ch

这是完整的示例

package main
import (
    "fmt"
    "time"
)
func ready(s int, c chan int) {
    fmt.Printf("Run func in a goroutine and wait for %v\n", s)
    time.Sleep(time.Second * time.Duration(s))
    fmt.Printf("Run func in a goroutine and wait for %v end\n", s)
    // Save wait interval to channel
    c <- s
    close(c)
}
func main() {
    ch := make(chan int)
    mainWaitSec := 2
    go ready(5, ch)
    fmt.Printf("Run Main function and wait for %v\n", mainWaitSec)
    time.Sleep(time.Second * time.Duration(mainWaitSec))
    fmt.Printf("Run Main function and wait for %v done\n", mainWaitSec)
    value, ok := <- ch
    fmt.Printf("Channel return value: %v\n", value)
    fmt.Printf("Channel ok returns: %v\n", ok)
}

执行结果如下

Run Main function and wait for 2
Run func in a goroutine and wait for 5
Run Main function and wait for 2 done
Run func in a goroutine and wait for 5 end
Channel return value: 5
Channel ok returns: true

此时的执行结果按照我们预期返回并结束

多并发使用示例

假设我们将主函数中再异步调用一次ready函数,并且执行时间为10秒会发生什么呢?

......
go ready(5, ch)
go ready(10, ch)
......

执行结果如下,我们并没有新增加的15秒并发ready函数退出就结束了,因为channel只接收到了第一次的ready函数的返回,后面无人处理后续管道返回,程序自然就退出返回了

Run Main function and wait for 2
Run func in a goroutine and wait for 5
Run func in a goroutine and wait for 10
Run Main function and wait for 2 done
Run func in a goroutine and wait for 5 end
Channel return value: 5
Channel ok returns: true

解决这个问题很简单,我们可以使用select或者for循环方式,结合管道是否打开的返回,动态等待管道返回

使用for方式

    for {
        value, ok := <- ch
        fmt.Printf("Channel return value: %v\n", value)
        fmt.Printf("Channel ok returns: %v\n", ok)
        if !ok {
            fmt.Println("No channel is open, break wait loop")
            break
        }
    }

这种方式下,for循环等价于,但是只能接受一个变量,不好判断channel结束,只能依靠额外的计数器进行判断

for value := range ch {
    // do someting
}

完整代码如下所示

package main
import (
    "fmt"
    "time"
)
func ready(s int, c chan int) {
    fmt.Printf("Run func in a goroutine and wait for %v\n", s)
    time.Sleep(time.Second * time.Duration(s))
    fmt.Printf("Run func in a goroutine and wait for %v end\n", s)
    // Save wait interval to channel
    c <- s
    close(c)
}
func main() {
    ch := make(chan int)
    mainWaitSec := 2
    go ready(5, ch)
    go ready(10, ch)
    fmt.Printf("Run Main function and wait for %v\n", mainWaitSec)
    time.Sleep(time.Second * time.Duration(mainWaitSec))
    fmt.Printf("Run Main function and wait for %v done\n", mainWaitSec)
    for {
        value, ok := <- ch
        fmt.Printf("Channel return value: %v\n", value)
        fmt.Printf("Channel ok returns: %v\n", ok)
        if !ok {
            fmt.Println("No channel is open, break wait loop")
            break
        }
    }
}

使用select-case方式

select块是为channel特殊设计的语法,它和switch语法非常相近。分支上它们都可以有多个case块和做多一个default块,但是也有很多不同

  • • select 到 括号{之间不得有任何表达式
  • • fallthrough关键字不能用在select里面
  • • 所有的case语句要么是channel的发送操作,要么就是channel的接收操作
  • • select里面的case语句是随机执行的,而不能是顺序执行的。设想如果第一个case语句对应的channel是非阻塞的话,case语句的顺序执行会导致后续的case语句一直得不到执行除非第一个case语句对应的channel里面的值都耗尽了。
  • • 如果所有case语句关联的操作都是阻塞的,default分支就会被执行。如果没有default分支,当前goroutine就会阻塞,当前的goroutine会挂接到所有关联的channel内部的协程队列上。所以说单个goroutine是可以同时挂接到多个channel上的,甚至可以同时挂接到同一个channel的发送协程队列和接收协程队列上。当一个阻塞的goroutine拿到了数据接触阻塞的时候,它会从所有相关的channel队列中移除掉。

我们来看以下实现,需要注意的一点,break执行需要在select之外,否则就是死循环,如果有多个channel,也可以分别利用不同的标志判断管道是否完全关闭。

    for {
        isOpen := true
        select {
        case value, ok := <- ch:
            fmt.Printf("Channel return value: %v\n", value)
            fmt.Printf("Channel ok returns: %v\n", ok)
            if !ok {
                isOpen = false
            }
        }
        if !isOpen {
            fmt.Println("No channel is open, break wait loop")
            break
        }
    }

完整代码如下所示

package main
import (
    "fmt"
    "time"
)
func ready(s int, c chan int) {
    fmt.Printf("Run func in a goroutine and wait for %v\n", s)
    time.Sleep(time.Second * time.Duration(s))
    fmt.Printf("Run func in a goroutine and wait for %v end\n", s)
    // Save wait interval to channel
    c <- s
    close(c)
}
func main() {
    ch := make(chan int)
    mainWaitSec := 2
    go ready(5, ch)
    go ready(10, ch)
    fmt.Printf("Run Main function and wait for %v\n", mainWaitSec)
    time.Sleep(time.Second * time.Duration(mainWaitSec))
    fmt.Printf("Run Main function and wait for %v done\n", mainWaitSec)
    for {
        isOpen := true
        select {
        case value, ok := <- ch:
            fmt.Printf("Channel return value: %v\n", value)
            fmt.Printf("Channel ok returns: %v\n", ok)
            if !ok {
                isOpen = false
            }
        }
        if !isOpen {
            fmt.Println("No channel is open, break wait loop")
            break
        }
    }
}

Deadlock异常

如果在循环内,不去判断channel关闭,会发生什么样的问题呢?错误如下:

[2022-05-30 17:36:42.005288]Wait Goroutine Demo for 15 second(s) done
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
    /root/workspace/go/test_goroutine_multiple_channels.go:49 +0x258
exit status 2

从错误信息我们可以看到,由于所有的goroutines都处于asleep状态,而主函数仍然在等待造成了死锁,所以必须显示的停止同步后,才能避免该问题的产生。

管道长度示例

上面已经提到管道可以指定长度方式来控制管道内的元素个数,下面通过一个示例来详细讲解

package main
import (
    "fmt"
    "time"
)
func write(ch chan int) {
    for i := 1; i <= 5; i++ {
        fmt.Printf("[%d]Before Send]Current length is %d\n", i, len(ch))
        ch <- i
        fmt.Printf("[%d][After Send]Current length is %d\n", i ,len(ch))
    }
    close(ch)
}
func main() {
    ch := make(chan int, 2)
    fmt.Printf("Channel's intial cap is %d\n", cap(ch))
    fmt.Printf("Channel's intial len is %d\n", len(ch))
    go write(ch)
    time.Sleep(5 * time.Second)
    for {
        fmt.Printf("[Before Recieve]Current length is %d\n", len(ch))
        value, ok := <- ch
        fmt.Printf("[%d][After Recieve]Current length is %d\n", value, len(ch))
        time.Sleep(2 * time.Second)
        if !ok {
            break
        }
    }
}

我们通过输出可以看到:

  • • 队列初始化后,容量(cap)大小为2,而实际长度是0,表示没有元素
  • • 在触发异步函数后,前两个元素顺利写入
  • • 而到3时,Send处于阻塞状态
  • • 直到主函数内完成Sleep,开始接受元素后,3才发送成功
  • • 通过这个示例可以清楚看到,当元素个数达到我们定义的范围时,则形成阻塞,直到管道内的数据被取走后程序再继续运行
Channel's intial cap is 2
Channel's intial len is 0
[1]Before Send]Current length is 0
[1][After Send]Current length is 1
[2]Before Send]Current length is 1
[2][After Send]Current length is 2
[3]Before Send]Current length is 2
[Before Recieve]Current length is 2
[1][After Recieve]Current length is 2
[3][After Send]Current length is 2
[4]Before Send]Current length is 2
[Before Recieve]Current length is 2
[2][After Recieve]Current length is 2
[4][After Send]Current length is 2
[5]Before Send]Current length is 2
[Before Recieve]Current length is 2
[3][After Recieve]Current length is 2
[5][After Send]Current length is 2
[Before Recieve]Current length is 2
[4][After Recieve]Current length is 1
[Before Recieve]Current length is 1
[5][After Recieve]Current length is 0
[Before Recieve]Current length is 0
[0][After Recieve]Current length is 0

参考文档

Channel运行逻辑及背后的一些原理,可以查看这篇文章

  • • Go Channel最佳实践之基本规则(https://zhuanlan.zhihu.com/p/32521576)

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/50272.html

(0)
上一篇 2024-04-25 09:45
下一篇 2024-04-28 07:18

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信