channel 是 Go 语言中的一个非常重要的特性, 是 Go 在并发编程中与大多数语言不同的一点,上文谈到大多数语言在解决并发同步问题上采用的是 “共享内存模型” 来处理(go 也支持该模型,但推崇能够用 channel 解决优先使用 channel)而 Go 将 CSP (有关 CSP 的详细可在上文中详细了解)思想引入,使用 gorountine + channel 代替传统的 线程 + 共享内存的方式来处理并发。


上文简单聊到 channel 的应用场景以及简单示例,本文就 channel 详细谈谈 channel 的实现与应用。

什么是 channel

《Concurrency In Go》 中谈到
Goroutine 解放了程序员,让我们更能贴近业务去思考问题。而不用考虑各种像线程库、线程开销、线程调度等等这些繁琐的底层问题,goroutine 天生替你解决好了。


Channel 则天生就可以和其他 channel 组合。我们可以把收集各种子系统结果的 channel 输入到同一个 channel。Channel 还可以和 select, cancel, timeout 结合起来。而 mutex 就没有这些功能。


Go 的并发原则非常优秀,目标就是简单:尽量使用 channel;把 goroutine 当作免费的资源,随便用。


在 Go 中 goroutine 负责执行并发任务, 而 channel 用于 goroutine 之间的同步、通信。

对于熟悉操作系统的朋友可以将 channel 类比为 pipeline (管道,多个 channels 串联), 一个 Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通信;由于它是线程安全的,所以用起来非常方便;channel 还提供“先进先出”的特性;它还能影响 goroutine 的阻塞和唤醒。 pipe pipe

当然 channel 的底层实现就是通过 mutex (锁)实现控制的。只是 channel 是更高层次的并发编程原语,封装了更多的功能。至于 mutex 与 channel 两者的选择可以参考下图

channel or mutex

channel 的使用

声明语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
chan T 
// 声明一个双向通道

chan<- T 
// 声明一个只能用于发送的通道

<-chan T 
// 声明一个只能用于接收的通道

// 非双向通道将在编译期检测,双向通道可以隐式转换为单向通道,反之不行
{
    bothway := make(chan int) // 定义一个双向类型为 int 的 chan
    out(bothway)
}

func out(out chan<- int) { // 隐式地转换为chan<- int
    x := 10
    out <- x
}

channel 是一个引用类型,所以在初始化之前,值为 nil。channel 使用 make 函数进行初始化。可以向它传递一个 int 值,代表 channel 缓冲区的大小(容量),构造出来的是一个缓冲型的 channel;不传或传 0 的,构造的就是一个非缓冲型的 channel。

1
2
3
ch := make(chan T) // no buffer

ch := make(chan T, 100) // use buffer len T[100]

关闭 channel

1
2
3
ch := make(chan int)

close(ch) // 通过 built-in 函数 close() 来关闭。

其中 * 重复关闭 channel 会导致 panic。 * 向关闭的 channel 发送数据会 panic。 * 从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。

channel 实现原理

对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。


Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”,带缓冲的则称为“异步模式”。


channel type

同步模式,双方都需要同步就绪,只有在双方(gorutine) 同时处于 ready 状态下,数据在两者间传输(实质为 内存拷贝)。否则,任意一方先发起操作,都会被挂起,等待另一方出现唤醒。当通过一个无缓存 Channels 发送数据时,接收者收到数据发生在唤醒发送者 goroutine 之前(happens before)

Tips: 《Go 语言圣经》在讨论并发编程时,当我们说 x 事件在 y 事件之前发生(happens before),我们并不是说x事件在时间上比 y 时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。

1
2
3
4
5
6
7
8
9
func main() {
    done := make(chan struct{})
    go func() {
        log.Println("done") // 此例中打印日志语句在 main goroutine 结束之前完成
        done <- struct{}{} // signal the main goroutine (发送方)
    }()

    <-done // wait for background goroutine to finish (接收方)
}


channel type

异步模式,在缓冲槽可以使用下(接收方: 缓冲区有数据、 发送方: 缓存区还有容量),发送和接受方都可以顺利进行。否则,操作的一方同样被挂起,等待另一方唤醒。

数据结构

源码 runtime/hchan 1.12.x

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

Q & A

  1. 下面这段程序输出什么 ?

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    func main() {
        ch := make(chan int) // 无缓冲区
        x := 10
    
        ch <- x // 当前 goroutine 将立即进入 asleep 状态,发生死锁
        fmt.Println(<-ch) 
    }
    // log: fatal error: all goroutines are asleep - deadlock!
    
    func main() {
        ch := make(chan int, 1) // 带缓冲区
        x := 10
    
        ch <- x
        fmt.Println(<-ch) // 10
    }

参考