通道(channel)

描述

  • 主要用于多个goroutine间传递数据.一个通道相当于一个先进先出(FIFO)的队列.
  • channel用来在协程[goroutine]之前传递数据,准确的说,是用来传递数据的所有权。
  • 一个设计良好的程序应该确保同一时刻channel里面的数据只会被同一个协程拥有,这样就可以避免并发带来的数据不安全问题[data races]。
  • 官方的go编译器限制channel最多能容纳到65535个元素,但我们不宜传递体积过大的元素值,因为channel的数据从进入到流出会涉及到数据拷贝操作。如果元素体积过大,最好的方法还是使用传递指针来取代传递值。

内部结构

每个channel内部实现都有三个队列。

  • 接收消息的协程队列。
    这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。
  • 发送消息的协程队列。
    这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。
  • 环形数据缓冲队列。
    这个环形数组的大小就是channel的容量。如果数组装满了,就表示channel满了,如果数组里一个值也没有,就表示channel是空的。
  • 对于一个阻塞型channel来说,它总是同时处于即满又空的状态。一个channel被所有使用它的协程所引用,也就是说,只要这两个装了协程的队列长度大于零,那么这个channel就永远不会被垃圾回收。另外,协程本身如果阻塞在channel的读写操作上,这个协程也永远不会被垃圾回收。

分类

  • 非缓冲通道
    make的时候第二个参数为0或者不填.
    无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递.数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转
package main

import (
	"fmt"
	"time"
)

func main()  {
	ch1 := make(chan int) //非缓冲信道
	go func() {
		ch1 <- 1
		fmt.Println("after 5 seconds, output this") //阻塞5秒后,主线程ch1信道才被接收,才输出该语句
	}()
	time.Sleep(5 * time.Second)
	<-ch1
	time.Sleep(2 * time.Second) //防止主线程结束,关闭其他线程
}
  • 缓冲通道
    make 的时候第一个参数大于0.
    在有容量的时候,发送和接收是不会互相依赖的.用异步的方式传递数据。
package main

import (
"fmt"
"time"
)

func main()  {
	ch1 := make(chan int, 1) //缓冲信道
	go func() {
		ch1 <- 1
		fmt.Println("not after 5 seconds, output this")
	}()
	time.Sleep(5 * time.Second)
	fmt.Println(<-ch1)
}

特性

  • 类型安全
  • 发送操作之间是互斥的,接收操作之间也是互斥的(多线程安全)
  • 进入通道的并不是在接收操作符右边的那个元素值,而是它的副本
  • 移出通道的是通道元素的副本
  • channel关闭后,如果还有数据还是可以正常读取的,等读取完后,v,ok := <- ch,其中ok是false,v是对应类型的零值
package main

import (
	"fmt"
	"time"
)

func main()  {
	ch1 := make(chan int, 5) //缓冲信道
	go func() {
		for i := 0; i < 5; i++ {
			ch1 <- i
			if i >= 4 {
				close(ch1) 
			}
		}
	}()
	time.Sleep(5 * time.Second)//保证上个goroutine结束,即ch1是关闭的
	i := 0
	for {
		v,ok := <-ch1
		fmt.Println(v,ok)
		i++
		if i > 7 { //若不加这段代码,会一直读
			break
		}
	}
}
输出:
0 true
1 true
2 true
3 true
4 true
0 false
0 false
0 false
  • 通道会阻塞goroutine
  • 关闭一个只读channel是非法的,编译器直接报错
  • 使用 v, ok <- ch 接收一个值。第二个遍历ok是可选的,它表示channel是否已关闭。接收值只会又两种结果,要么成功要么阻塞,而永远也不会引发panic
  • 不同于array/slice/map上的for-range,channel的for-range只允许有一个变量
package main

import (
	"fmt"
)

func main()  {
	ch1 := make(chan int, 5) //缓冲信道
	go func() {
		for i := 0; i < 5; i++ {
			ch1 <- i
			if i >= 4 {
				close(ch1) //如果不加这句,会因为range一直读一个未关闭channel,而死锁
			}
		}
	}()
	for v := range ch1 { //一直接收channel数据,直到channel关闭
		fmt.Println(v)
	}
}

阻塞的Case

  • 通道容量已满情况,执行写入
  • 通道没有数据,执行读取
  • 对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行.
  • 对于值为nil的通道,若程序只有一个goroutine,向其中发送数据或接收数据,都会阻塞,程序会死锁;若程序有多个goroutine,只会阻塞那个向nil通道发送或接收数据的goroutine,程序不会panic

panic的Case

  • 关闭已经关闭的channel
  • 已经关闭的channel发送数据

单向通道

make(chan<- int, 1)
make(<-chan int, 1)
  • 作用
    a.作为函数参数,从而约束函数体内的行为;
    b.作为函数返回值,从而约束返回后的行为;
//函数返回一个单向通道,那么该通道只能接收,不能发送
func getIntChan() <-chan int {
 ch := make(chan int, 5)
 for i := 0; i < 5; i++ {
 ch <- i
 }
 close(ch)
 return ch
}
func main() {
 ch := getIntChan()
 ch <- 6 //编译报错send to receive-only type <-chan int
}

正常通道关闭

  • 从channel的接收协程队列中移除所有的goroutine,并唤醒它们。

  • 从channel的接收协程队列中移除所有的goroutine,并唤醒它们。

  • 一个已关闭的channel内部的缓冲数组可能不是空的,没有接收的这些值会导致channel对象永远不会被垃圾回收。

总结

通道(channel)

  • 如果channel关闭了,那么它的接收和发送协程队列必然空了,但是它的缓冲数组可能还没有空。
  • channel的接收协程队列和缓冲数组,同一个时间必然有一个是空的
  • channel的缓冲数组如果未满,那么它的发送协程队列必然是空的
  • 对于缓冲型channel,同一时间它的接收和发送协程队列,必然有一个是空的
  • 对于非缓冲型channel,一般来说同一时间它的接收和发送协程队列,也必然有一个是空的,但是有一个例外,那就是当它的发送操作和接收操作在同一个select块里出现的时候,两个队列都不是空的。
上一篇:《数据密集型应用系统设计》读书笔记-ch1可靠、可扩展与可维护的应用系统


下一篇:ch1_5_1统计最大最小元素的平均比较次数