chan

Go 语言推荐使用 消息传递 实现 并发通信,这种消息通信机制被称为 channel,中文译作「通道」,可理解为传递消息的通道。

chan 是 Go 语言 中的一个核心 类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。

chan 的本质是一个队列,且 chan 是线程安全的, 也就是自带 锁 的功能。

  • chan 是 goroutine 之间的通信机制
  • 一个 chan 可以让 一个 goroutine 通过它 给 另一个 goroutine 发送信息。
  • 每个 chan 都对应一个特殊的数据类型。

Go 语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。

声明通道时,需要指定将要被共享的数据的类型。
可以通过通道共享:

  • 内置类型
  • 命名类型
  • 结构类型
  • 引用类型的值或者指针

image.png

通道声明和初始化

第一种方式 chan & make

setp1: 用 chan 声明通道类型变量

语法

  1. var chanName chan chanType

参数

参数 描述
var 声明变量使用的关键字
chanName 管道变量名
chan 声明管道变量类型使用的关键字
chanType 管道变量的具体类型

说明

  • 声明一个 chan 类型的 变量 ,变量名为 chanName
  • 变量的类型为 chan chanType
  • chanName 的值为 nil,声明之后需要使用 make 创建完毕之后,才可以使用

声明一个通道类型变量 ch,并且通道中只能传递 int 类型数据,表达式如下:

  1. var ch chan int

还可以通过如下方式声明通道数组、切片、字典,以下声明方式表示 chs 中的元素都是 chan int 类型的通道:

  1. var chs [10]chan int
  2. var chs []chan int
  3. var chs map[string]chan int

step2: 用 make 初始化通道

通道是引用类型,需要使用 make 进行创建,格式如下:

  1. chanName = make(chan chanType, bufferSize)
  • 使用 make 创建一个类型为 chan chanType 的管道 chanName
  • 第二个参数是可选的,用于指定通道最多可以缓存多少个元素,默认值是 0,无缓冲通道
  1. var court chan int
  2. court = make(chan int)

第二种 make 直接声明 并 初始化

实际编码时,我们更多使用的是下面这种快捷方式同时声明和初始化通道类型

  1. chanName := make(chan chanType, bufferSize)
  1. package main
  2. import "fmt"
  3. func main() {
  4. // 创建一个3个元素缓冲大小的整型通道
  5. ch := make(chan int, 3)
  6. // 查看当前通道的大小
  7. // 带缓冲的通道在创建完成时,内部的元素是空的,因此使用 len() 获取到的返回值为 0
  8. fmt.Println(len(ch)) // 0
  9. // 发送3个整型元素到通道
  10. ch <- 1
  11. ch <- 2
  12. ch <- 3
  13. // 查看当前通道的大小
  14. fmt.Println(len(ch)) // 3
  15. }

chan 发送/接收数据和关闭通道

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。我们通过调用内置的close函数来关闭通道。

发送数据

  1. // 发送数据
  2. chanName <- "msg" // 向管道发送字符串msg
  • chanName 管道变量
  • “msg” 要发送的管道类型的数据

接收数据

  1. // 接收数据
  2. msg := <- chanName
  • msg 要接受的数据存放的变量
  • chanName 管道变量

关闭通道

  1. close(chanName)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭

  1. msg, ok := <- chanName
  • msg 要接受的数据存放的变量
  • ok 表示管道是否关闭,如果为 false,则表明管道已经关闭
  • chanName 管道变量
  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. chanStr := make(chan string)
  8. // 创建一个匿名协程
  9. go func() {
  10. // 发送数据:Hello Golang"
  11. chanStr <- "Hello Golang"
  12. time.Sleep(time.Millisecond)
  13. close(chanStr)
  14. }()
  15. for {
  16. // 接受数据: Hello Golang
  17. msg, isOK := <- chanStr
  18. if (isOK) {
  19. // 打印
  20. fmt.Println("chan Msg =", msg) // chan Msg = Hello Golang
  21. } else {
  22. fmt.Println("通道已关闭")
  23. break
  24. }
  25. }
  26. /*
  27. chan Msg = Hello Golang
  28. 通道已关闭
  29. */
  30. }

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

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

带缓冲通道和无缓冲通道

分类

Golang 中的 channel 有两种类型,分别为:无缓冲 channel 和 有缓冲 channel。

  1. // 无缓冲,默认值为0
  2. chanName = make(chan chanType) //chanName = make(chan chanType, 0)
  3. // 有缓冲 缓冲大小为10
  4. chanName = make(chan chanType, 10)

无缓冲的通道

是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。又称为阻塞的通道

有缓冲的通道

是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。

带通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

区别

无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换,而有缓冲的通道没有这种保证。

在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。
带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。

如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。

无缓冲通道保证收发过程同步。而有缓冲是异步的收发过程,因此效率可以有明显的提升。

无缓冲收发过程类似于快递员给你电话让你下楼取快递,整个递交快递的过程是同步发生的,你和快递员不见不散。

但这样做快递员就必须等待所有人下楼完成操作后才能完成所有投递工作。

如果快递员将快递放入快递柜中,并通知用户来取,快递员和用户就成了异步收发过程,效率可以有明显的提升。带缓冲的通道就是这样的一个“快递柜”。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. //创建一个有缓存的channel
  8. ch := make(chan int, 3)
  9. //len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
  10. fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))
  11. //新建协程
  12. go func() {
  13. for i := 0; i < 10; i++ {
  14. ch <- i //往chan写内容
  15. fmt.Printf("子协程[%d]: len(ch) = %d, cap(ch)= %d\n", i, len(ch), cap(ch))
  16. }
  17. }()
  18. time.Sleep(1 * time.Second)
  19. for i := 0; i < 10; i++ {
  20. num := <-ch //读管道中内容,没有内容前,阻塞
  21. fmt.Println("num = ", num)
  22. }
  23. }
  24. /*
  25. len(ch) = 0, cap(ch)= 3
  26. 子协程[0]: len(ch) = 1, cap(ch)= 3
  27. 子协程[1]: len(ch) = 2, cap(ch)= 3
  28. 子协程[2]: len(ch) = 3, cap(ch)= 3
  29. num = 0
  30. num = 1
  31. num = 2
  32. num = 3
  33. 子协程[3]: len(ch) = 3, cap(ch)= 3
  34. 子协程[4]: len(ch) = 0, cap(ch)= 3
  35. 子协程[5]: len(ch) = 1, cap(ch)= 3
  36. 子协程[6]: len(ch) = 2, cap(ch)= 3
  37. 子协程[7]: len(ch) = 3, cap(ch)= 3
  38. num = 4
  39. num = 5
  40. num = 6
  41. num = 7
  42. num = 8
  43. 子协程[8]: len(ch) = 0, cap(ch)= 3
  44. 子协程[9]: len(ch) = 0, cap(ch)= 3
  45. num = 9
  46. */
  • 定义了一个有缓冲的通道,大小为3
  • ch <- i 子协程 往通道里写入数据是,超过3个就写不入了 会阻塞,所以会打印35-37行
  • num := <-ch 主协程 从通道 读取数据,因为通道是队列,先进先出,读取一个,缓冲就有空间,可以写入

为什么Go语言对通道要限制长度而不提供无限长度的通道?

我们知道通道(channel)是在两个 goroutine 间通信的桥梁。
使用 goroutine 的代码必然有一方提供数据,一方消费数据。
当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果通道不限制长度,那么内存将不断膨胀直到应用崩溃。

因此,限制通道的长度有利于约束数据提供方的供给速度,供给数据量必须在消费方处理量+通道长度的范围内,才能正常地处理数据。

阻塞条件

带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看作是长度永远为 0 的带缓冲通道。
因此根据这个特性,带缓冲通道在下面列举的情况下依然会发生阻塞:

  • 带缓冲通道被填满时,尝试再次发送数据时发生阻塞。
  • 带缓冲通道为空时,尝试接收数据时发生阻塞。

资源竞争问题

在上一章使用咖啡机时A和B并发执行,会有交叉使用,而且会有插队的情况。
那如果现在还是想A人员优先使用,使用完后再告诉B队人员可以冲咖啡了,怎么做呢?

我们可以通过channel实现同步,具体看代码注释

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. var (
  7. ListA = []string{"小明", "小红", "小王", "宝宝", "娜娜", "拉拉", "琪琪"}
  8. ListB = []string{"威廉", "巴克", "查理", "哈利", "哈瑞", "米菲", "珍妮", "米奇"}
  9. )
  10. //全局变量,创建一个channel
  11. var ch = make(chan string)
  12. func makeCoffe (list []string, from string) {
  13. for index, name := range list {
  14. if (from == "A") {
  15. fmt.Printf("A队[%d]: %s 使用咖啡机 \n", index, name)
  16. } else if (from == "B") {
  17. fmt.Printf("B队[%d]: %s 使用咖啡机 \n", index, name)
  18. }
  19. time.Sleep(time.Second)
  20. }
  21. }
  22. func task1() {
  23. makeCoffe(ListA, "A")
  24. // 不写入数据 B队没法冲咖啡
  25. ch <- "A队已经冲好咖啡,B队你们可以冲咖了"
  26. }
  27. func task2() {
  28. msg := <- ch // 如果管道没有数据,就会阻塞 B队没法冲咖啡
  29. fmt.Println(msg) // 打印A队传递过来的消息
  30. makeCoffe(ListB, "B")
  31. close(ch)
  32. msg, ok := <- ch;
  33. if ok == false {
  34. fmt.Println("管道已关闭")
  35. }
  36. fmt.Println(msg) // 打印A队传递过来的消息
  37. }
  38. func main() {
  39. // 子协程
  40. go task1()
  41. go task2()
  42. // 主协程
  43. for {
  44. ;
  45. }
  46. /**
  47. A队[0]: 小明 使用咖啡机
  48. A队[1]: 小红 使用咖啡机
  49. A队[2]: 小王 使用咖啡机
  50. A队[3]: 宝宝 使用咖啡机
  51. A队[4]: 娜娜 使用咖啡机
  52. A队[5]: 拉拉 使用咖啡机
  53. A队[6]: 琪琪 使用咖啡机
  54. A队已经冲好咖啡,B队你们可以冲咖了
  55. B队[0]: 威廉 使用咖啡机
  56. B队[1]: 巴克 使用咖啡机
  57. B队[2]: 查理 使用咖啡机
  58. B队[3]: 哈利 使用咖啡机
  59. B队[4]: 哈瑞 使用咖啡机
  60. B队[5]: 米菲 使用咖啡机
  61. B队[6]: 珍妮 使用咖啡机
  62. B队[7]: 米奇 使用咖啡机
  63. 管道已关闭
  64. */
  65. }

前面的例子中,在主协程里都需要这一样段代码 for { ; } 子协程才能运行起来。
现在改用通道的方式

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. //创建channel
  8. chStr := make(chan string)
  9. defer fmt.Println("主协结束")
  10. go func() {
  11. defer fmt.Println("子协程调用完毕")
  12. for i := 0; i < 2; i++ {
  13. fmt.Println("子协程 i = ", i)
  14. time.Sleep(time.Second)
  15. }
  16. // 向通道写入数据
  17. // 如果 这里不写入数据 那么直接造成死锁
  18. // chStr <- "子协程任务完成结束"
  19. }()
  20. str := <-chStr //没有数据前,阻塞
  21. fmt.Println("str = ", str)
  22. }

打印结果

  1. // 没有向通道写入数据 死锁
  2. 子协程 i = 0
  3. 子协程 i = 1
  4. 子协程调用完毕
  5. fatal error: all goroutines are asleep - deadlock!
  6. goroutine 1 [chan receive]:
  7. main.main()
  8. // 向通道写入数据 子/主协程正常退出
  9. 子协程 i = 0
  10. 子协程 i = 1
  11. 子协程调用完毕
  12. str = 子协程任务完成结束
  13. 主协结束

参考:
http://c.biancheng.net/view/99.html