【golang】channel通道

235 阅读5分钟

简介-是什么、有什么用

channel字面意思是“通道”,用于goroutine之间进行通信、同步

Goroutine 和 channel 是 Go 语言并发编程的两大基石。Goroutine用于执行并发任务,channel用于 goroutine之间的通信、同步。

看个简单demo:

package main

import (
   "log"
   "time"
)

var logger = log.Default()

func main() {
   c1 := make(chan int)

   go func() {
      logger.Println("go sleep start")
      time.Sleep(3 * time.Second)
      logger.Println("go sleep end, send start")
      c1 <- 1
      logger.Println("send end")
   }()

   logger.Println("main receive start")
   i, ok := <-c1
   logger.Printf("main receive end, i:%d, ok:%t\n", i, ok)
}

运行程序可以看到main协程从通道读取值时会阻塞,直到另一个协程往通道发送数据,才会唤醒main协程。

CSP模型

与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模型

共享内存存在竞态问题,需要加锁同步,会造成性能问题。

CSP (Communicating Sequential Process ),即通信顺序进程,一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型

强调通信,有这么一句话:

不要通过共享内存来通信,而要通过通信来实现内存共享。

Go语言通过协程Goroutine和通道Channel实现了 CSP 模型。go语言并没有完全实现了CSP模型的所有理论,仅借用了 processchannel这两个概念(分别对应go语言的goroutine和channel):process是并发执行的实体,每个实体之间通过channel通讯来实现数据共享。

channel详解

channel具有如下特性:

  • 并发安全
  • 先进先出
  • 能阻塞和唤醒goroutine

channel类型

channel是go语言的一种特殊类型,它是一个引用类型,因此未初始化时其默认零值是nil

  1. 定义channel变量:
var 变量名称 chan 元素类型
  • 元素类型,是指channel通道中元素的具体类型。

例子:

var c1 chan int
var c2 chan string
var c3 chan bool
var c4 chan []int
  1. 初始化channel,使用make

未初始化,默认零值是nilnil通道发送或接收都会一直阻塞

make(chan 元素类型, [缓冲区容量])
  • 缓冲区容量:是可选参数,不指定或为0则创建无缓冲通道,指定且大于0则创建有缓冲通道。

例子:

c1 := make(chan int)
c2 := make(chan int, 1)

channel的操作

三种操作:

  1. 发送(即写操作):往通道中发送数据
  1. 接收(即读操作):从通道中读取数据
  1. 关闭

发送和接收使用<-符号。

发送

ch <- 10 // 把10发送到ch中

发送什么时候会阻塞?
通道未关闭缓冲区满时,发送数据就会阻塞,直到有其他协程消费数据使缓冲区不满才不再阻塞。

接收

x := <- ch // 接收并赋值
<-ch       // 仅接收

接收什么时候会阻塞?
通道未关闭缓冲区空无数据可读)时,接收数据就会阻塞,直到有其他协程向通道生产数据使缓冲区不空、有数据可读时才不再阻塞。


多返回值模式,接收操作可以使用多返回值模式:

value, ok := <- ch
  • ok:表示是否成功从通道中读取数据。false,即未成功读取,表示通道为空、无数据可读了,且此时通道肯定关闭了,不然接收操作会阻塞。

注意:接收操作不阻塞,只有两种情况:1. 有数据可读、通道不空;2. 若无数据可读,则通道必须关闭了

  • value:从通道中读取的值,若ok为false,则value是通道元素的数据类型的零值。

个人认为,多返回值模式的作用:如果ok返回false,则表示通道空了且已关闭

nil通道

chan类型的默认零值是nilnil通道进行读、写都会阻塞

关闭通道

使用close函数

close(ch)

注意:

  1. 通常由发送方执行关闭操作
  1. 关闭通道不是必须的(不像关闭文件是必须的),一般仅在接收方明确需要关闭信号时才执行关闭操作

关闭后的通道有如下特点:

  1. 不能再发送,再发送数据会panic
  1. 可以接收。通道被关闭,仍然可以读取未读完的数据当通道为空(即没有数据了),再读取也不会造成阻塞,而是会返回对应数据类型的零值

    重要:已关闭通道,一定不会阻塞读取操作

  1. 重复关闭会panic

close最佳实践:

  1. 非必要不要close
  2. 比较常见的是将close作为一种通知机制,通过close告诉消费者我关闭了,此时消费者消费就不会再阻塞了

for range接收通道数据

for range可不断从通道中接收数据,当通道为空且已关闭了,会退出循环(因为为空且已关闭的通道是不可能再有数据了);当通道为空但未关闭,会阻塞直到通道有数据。

示例:

func main() {
   c1 := make(chan int, 3)

   go func() {
      for i := 0; i < 10; i++ {
         <-time.After(time.Second)
         c1 <- i
      }

      <-time.After(5 * time.Second)

      for i := 90; i < 100; i++ {
         <-time.After(time.Second)
         c1 <- i
      }

      <-time.After(5 * time.Second)
      log.Default().Println("---close chan---")
      close(c1)
   }()

   go func() {
      for i := range c1 {
         log.Default().Println(i)
      }
      log.Default().Println("---chan closed---")
   }()

   for {

   }
}

单向通道

是什么?

只能发送只能接收的通道就是单向通道。


为什么要单向通道?

某些场景下我们想限制使用者对通道的操作:只允许发送或接收,为了表明这种意图并防止被滥用,所以go语言提供了单向channel。


如何使用?

定义单向通道

箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收

「只接收channel」不允许close,因为一般都是发送方来close通道。


通道类型转换

双向通道可以转成单向通道(是隐式转换),但反之不行。


示例:

生产者只能向channel发送数据,消费者只能从channel接收数据,所以使用单向channel

func main() {
   c1 := make(chan int, 1)
   go producer(c1)
   go consumer(c1)
   for {
   }
}
func producer(ch chan<- int) {
   for i := 0; i < 10; i++ {
      <-time.After(time.Second)
      ch <- i
   }
   log.Default().Println("producer. close chan")
   // 发送通道可close
   close(ch)
}

func consumer(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consume:", i)
      // 接收通过不能close
      //close(ch)
   }
   log.Default().Println("consumer. chan closed")
}

select多路复用

作用:

可以同时监听多个channel的发送或接收操作。


如何使用?

  1. 类似switch语句,select也有一系列case分支一个default分支,每个case分支可以监听一个channel的发送或接收操作若可接收或可发送,则匹配该case分支;没有任何case匹配,则走default分支。
  1. 若没有default分支,select会一直阻塞直到某个case匹配。
  1. 若没有case也没有default分支,select会一直阻塞
  1. 若同时有多个case满足,select会随机选择一个case执行。

使用方式如下:

select {
case <-ch1:
    //...
case data := <-ch2:
    //...
case ch3 <- 10:    
    //...
default:
    //默认操作
}

示例:

func main() {
   ch := make(chan int, 1)
   for i := 0; i < 10; i++ {
      select {
      case ch <- i:
         log.Default().Println("send")
      case x := <-ch:
         log.Default().Println("receive, x:", x)
      default:
         log.Default().Println("default")
      }

      // 会panic panic: send on closed channel
      //if i == 0 {
      // close(ch)
      //}
   }
}

无缓冲channel和有缓冲channel

make(chan 元素类型, [缓冲区容量])

创建channel时:

  • 未指定缓冲区容量或为0,则创建的是无缓冲channel
  • 指定了缓冲区容量且大于0,则创建的是有缓冲channel

无缓冲channel

没有缓冲区,channel中不能缓存数据

  • 发送操作一定会阻塞,直到有人接收
  • 接收操作一定会阻塞,直到有人发送

有缓冲channel

有缓冲区,channel中可以缓存数据

  • 仅当缓冲区满时,发送操作才阻塞,直到有人接收
  • 仅当缓冲区空时,接收操作才阻塞,直到有人发送

len和cap方法

  • len方法可以返回通道中元素的个数
  • cap方法可以返回通道的缓冲区容量

对于「无缓冲channel」,len和cap都是0

问题

channel有一个发送方,多个接收方怎么办?

多个接收方共同消费channel中的数据。

示例:

func main() {
   ch1 := make(chan int, 1)
   go consumer(ch1)
   go consumer2(ch1)
   go producer(ch1)
   for {

   }
}
func producer(ch chan<- int) {
   for i := 0; i < 10; i++ {
      <-time.After(time.Second)
      ch <- i
   }
   log.Default().Println("producer. close chan")
   // 发送通道可close
   close(ch)
   log.Default().Println("producer. end")
}

func consumer(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consumer consume:", i)
   }
   log.Default().Println("consumer. chan closed")
}
func consumer2(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consumer2 consume:", i)
   }
   log.Default().Println("consumer2. chan closed")
}

运行结果:

2022/10/30 01:46:46 consumer consume: 0
2022/10/30 01:46:47 consumer2 consume: 1
2022/10/30 01:46:48 consumer consume: 2
2022/10/30 01:46:49 consumer2 consume: 3
2022/10/30 01:46:50 consumer consume: 4
2022/10/30 01:46:51 consumer2 consume: 5
2022/10/30 01:46:52 consumer consume: 6
2022/10/30 01:46:53 consumer2 consume: 7
2022/10/30 01:46:54 consumer consume: 8
2022/10/30 01:46:55 consumer2 consume: 9
2022/10/30 01:46:55 producer. close chan
2022/10/30 01:46:55 producer. end
2022/10/30 01:46:55 consumer. chan closed
2022/10/30 01:46:55 consumer2. chan closed

goroutine泄漏

goroutine泄漏是什么?

是指goroutine一直阻塞而无法被回收

不正确的使用channel可能导致goroutine泄漏,如下:

func main() {
   ch := make(chan int)

   go test(ch)
   select {
   case <-ch:
      log.Default().Println("receive")
   case <-time.After(time.Second):
      log.Default().Println("time")
   }

   for {

   }
}

func test(ch chan int) {
   log.Default().Println("test run")
   <-time.After(3 * time.Second)
   ch <- 1
   log.Default().Println("test end")
}

运行结果:

2022/10/30 01:55:17 test run
2022/10/30 01:55:18 time

发生了goroutine泄漏,test方法的goroutine一直阻塞了,因为main gorutine无法接收通道

源码

待读

参考

interview.wzcu.com/Golang/CSP.…