golang之Channel

心已赠人 2022-05-30 08:58 356阅读 0赞

Channel是Go中的一个核心类型,可以将其看成一个管道,通过它并发单元就可以发送或者接收数据进行通信(communication)。

Do not communicate by sharing memory; instead, share memory by communicating.

channel基础知识

创建channel

使用内建函数make创建channel:

  1. unBufferChan := make(chan int) // 1 无缓冲的channel
  2. bufferChan := make(chan int, N) // 2 带缓冲的channel
  • 无缓冲: 发送和接收动作是同时发生的。如果goroutine读取channel(<-channel),则发送者(channel<-)会一直阻塞。
  • 缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。

写出以下代码打印的结果:

  1. func main() {
  2. var x chan int
  3. go func() {
  4. x <- 1
  5. }()
  6. <-x
  7. }

结果分析:往一个nil channel中发送数据会一直被阻塞,从一个nil channel中接收数据会一直被block,所以才会产生如下死锁的现象。

  1. fatal error: all goroutines are asleep - deadlock!
  2. goroutine 1 [chan receive (nil chan)]:
  3. main.main()
  4. /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60
  5. goroutine 4 [chan send (nil chan)]:
  6. main.main.func1(0x0)fatal error: all goroutines are asleep - deadlock!

结论:channl 使用要小心,使用前切记要初始化,初始化函数用make。

channel读写操作

  1. ch := make(chan int, 10)
  2. // 读操作
  3. x <- ch
  4. // 写操作
  5. ch <- x

channel关闭

channel可以通过内建函数close()来关闭:

  1. ch := make(chan int)
  2. // 关闭
  3. close(ch)

关闭channel注意事项

  • 重复关闭channel会导致panic。
  • 向关闭的channel发送数据会panic。
  • 从关闭的channel读数据不会panic,读取出channel中已有的数据之后再读就是channl类型的默认值,比如chan bool类型的channel关闭之后读取的值为false。

区分channl中读取的默认值还是channl中传输的值,采用ok_idiom方式:

  1. ch := make(chan int, 10)
  2. ...
  3. close(ch)
  4. // ok-idiom
  5. val, ok := <-ch
  6. if ok == false {
  7. // channel closed
  8. }

channel使用

select

golang的select的功能和select 、poll、epoll类似,就是监听IO操作,当IO操作发生时触发响应的动作(select 的case里的语句只能是IO操作) 。select使用一般配合for循环使用。

示例1
  1. ch1 := make (chan int, 1)
  2. ch2 := make (chan int, 1)
  3. ...
  4. select {
  5. case <-ch1:
  6. fmt.Println("ch1 pop one element")
  7. case <-ch2:
  8. fmt.Println("ch2 pop one element")
  9. }

结果

此示例里面 select 会一直等待等到某个 case 语句完成, 也就是等到成功从 ch1 或者 ch2 中读到数据。 则 select 语句结束。

示例2—使用select实现timeout机制

import "time" import "fmt" func main() { c1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second * 1): fmt.Println("timeout 1") } }

结果

这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1

示例3—带有default的select

““go
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

select {
case <-ch1:
fmt.Println(“ch1 pop one element”)
case <-ch2:
fmt.Println(“ch2 pop one element”)
default:
fmt.Println(“default”)
}
““

结果

ch1 和 ch2 都为空,所以 case1 和 case2 都不会读取成功。 则 select 执行 default 语句。

作用

因为这个 default 特性, 我们可以使用 select 语句来检测 chan 是否已满。示例代码如下:

  1. ch := make (chan int, 1)
  2. ch <- 1
  3. select {
  4. case ch <- 2:
  5. default:
  6. fmt.Println("channel is full !")
  7. }
  8. //ch 插入 1 的时候已经满了, 当 ch 要插入 2 的时候,发现 ch 已经满了(case1 阻塞住), 则 select 执行 default 语句。 这样就可以实现对 channel 是否已满的检测, 而不是一直等待。

range channel

range channel 可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。

  1. func consumer(ch chan int) {
  2. for x := range ch {
  3. fmt.Println(x)
  4. ...
  5. }
  6. }
  7. func producer(ch chan int) {
  8. for _, v := range values {
  9. ch <- v
  10. }
  11. }

单向channel

单向channel,顾名思义只能写或读的channel。但是在实际使用中用处不大,单向channel主要用于函数声明。比如:

  1. func foo(ch chan<- int) <-chan int {...}
  2. //foo 的形参是一个只能写的 channel,那么就表示函数 foo 只会对 ch 进行写,当然你传入的参数可以是个普通 channel。foo 的返回值是一个只能读的 channel,那么表示 foo 的返回值规范用法就是只能读取。

单向channel在功能上和普通的channel并没有太大区别。但是使用单向channel编程体现一种非常优秀的编程范式:convention over configuration(约定由于配置)。

channel的典型用法

同步goroutine

chan是go里的第一对象,所以可以把chan传入chan中。具体示例如下:

““go
func main() {
g := make(chan int)
quit := make(chan chan bool)
go Channel(g, quit)
for i := 0; i < 5; i++ {
g<-i
}
wait := make(chan bool)
quit <-wait
<-wait //等Channel函数中传入的bool类型的chan有值写入的时候才会继续执行
fmt.Println(“Main Quit”)
}

func Channel(g chan int, quit chan chan bool) {
for {
select {
case i := <-g:
fmt.Println(i)
case c := <-quit: //接收main函数传递的bool类型的channel
c <- true //向其中写入值true
fmt.Println(“channel function Quit”)
return
}
}
}

/* output*/
0
1
2
3
4
B Quit
Main Quit
““

基于channel、gorutine实现工作池

worker工作池负责处理任务jobs,然后将加工后的结果写入到result,所以此处需要两个通道。jobs负责任务的传递,results负责结果的传输。这里启动3个worker,初始是阻塞的,因为还没有任务传递。然后传递9个任务到jobs中并关闭通道表示全部任务发送完毕。最后调用sumResult()统计任务返回的结果。

  1. func worker(id int, jobs chan int, results chan int) {
  2. for j := range jobs {
  3. fmt.Println("worker", id, "process job", j)
  4. time.Sleep(time.Second)
  5. results <- j * 2
  6. }
  7. }
  8. func sumResult(results chan int) {
  9. var sum int
  10. for a := 1; a <= 9; a++ {
  11. s := <-results
  12. sum += s
  13. fmt.Println("sum", sum, "s", s)
  14. }
  15. }
  16. func main() {
  17. jobs := make(chan int, 100)
  18. results := make(chan int, 100)
  19. for w := 1; w <= 3; w++ {
  20. go worker(w, jobs, results)
  21. }
  22. for j := 1; j <= 9; j++ {
  23. jobs <- j
  24. }
  25. close(jobs)
  26. sumResult(results)
  27. }

channel 问题

问题: [Is it OK to leave a channel open?

解答

  1. It's OK to leave a Go channel open forever and never close it. When the channel is no longer used, it will be garbage collected.
  2. Note that it is only necessary to close a channel if the receiver is looking for a close. Closing the channel is a control signal on the channel indicating that no more data follows.

参考资料

  • golang的select典型用法
  • 3种优雅的Go channel用法
  • [Go Concurrency Patterns: Pipelines and cancellation](https://blog.golang.org/pipelines)
  • Visualizing Concurrency in Go
  • 深入理解Go Channel
  • Go Channel详解
  • Go by Example中文:工作池

发表评论

表情:
评论列表 (有 0 条评论,356人围观)

还没有评论,来说两句吧...

相关阅读

    相关 golangchannel的超时

    基本语法 通道声明和初始化 我们可以通过`chan`关键字来声明通道类型变量 var ch chan int // 声明一个通道类型变量 ch,并且通道

    相关 golang channel 的使用

    本文对channel使用中的几个疑惑,以例子的形式加以说明。 普通channel 缺省情况下,发送和接收会一直阻塞着,直到另一方准备好. 例如: pack

    相关 golangChannel

    Channel是Go中的一个核心类型,可以将其看成一个管道,通过它并发单元就可以发送或者接收数据进行通信(communication)。 `Do not communicat