基本介绍
通道(chan)是goroutine之间传递特定值的通信机制。它属于通信顺序进程
并发模式(Communicating sequential processes,CSP)。go语言中还有另一种并发模式,即共享内存多线程的传统模型。
三种类型划分
接下来讨论chan的不同类型,从三个角度去分析:
缓冲/非缓冲(buffered/unbuffered)
chan是一个有长度的容器,通过对长度的限定,可以定义不同类型的通道。
对于声明长度的chan,称之为缓冲通道
ch := make(chan int, 1024) // buffered chan
对于没有声明长度的chan,称之为非缓冲通道
ch := make(chan int) // unbuffered chan
两种类型的chan会有不同的特性
- 非缓冲chan对于读写两端的goroutine都会阻塞。
- 缓冲chan在容量为空时,读端goroutine会阻塞;在容量未满时,读写两端都不会阻塞;但是容量满了之后,写端会阻塞
获取/发送(send/receive)
对于chan有获取/发送两种操作,在声明的时候可以限定使用的方式
- 获取/发送双向chan:
// chan T
type goWorker struct {
pool *Pool
task chan func()
recycleTime time.Time
}
- 只获取chan:
// <-chan T 常见作为参数传递
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
- 只发送chan:
// chan<-T 使用较少
func signalError(c chan<- error, err error) {
select {
case c <- err:
default:
}
}
关闭(close)
chan不仅有长度,还可以被关闭,在被关闭后停止传递特定值。由此引出chan不同的状态:空/empty
,满/full
,关闭/closed
chan在关闭状态下,读取操作得到通道对应类型的空值,发送操作则会触发panic
done := make(chan struct{}, 1)
close(done)
d := <-done
// {}
done := make(chan struct{}, 1)
close(done)
done <- struct{}{}
//panic: send on closed channel
原理
chan在运行时内部由结构体runtime.hchan
实现,想要深入了解可以阅读 源码,还可以参考阅读 draveness的Go语言设计与实现。在此不做赘述。
使用指南
这是本文的核心章节,通过举例来展示chan的使用场景。
使用方法
chan在实际使用时,通常会结合for
和select
- for/range - 持续循环
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
- select - 随机选择
select {
case <-time.Tick(time.Minute * time.Duration(bp.testMinutes)):
case <-signalChan:
}
使用案例
- chan常见用于传递基本类型和结构体。
自带库:time/After,context/WithCancel
type Timer struct {
C <-chan Time
r runtimeTimer
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
- chan不仅可以传递基本类型与结构体,还可以传递函数。
开源库:ants/worker
type goWorker struct {
// pool who owns this worker.
pool *Pool
// task is a job should be done.
task chan func()
// recycleTime will be update when putting a worker back into queue.
recycleTime time.Time
}
- 在实际工程中,通常会在主程序入口进行signal监听,据此实现安全退出机制。
系统信号:os/signal
c := make(chan os.Signal, 1)
signal.Notify(c)
s := <-c
fmt.Println("Got signal:", s)
总结:在实际工程代码中,常见三种值类型
- 基本类型&结构体
- os.Signal
- func()
工程问题
工程开发中会遇到类似这样的问题:何时使用chan?对比mq如何取舍?
整理如下几点使用chan考虑的因素:
- 所在goroutine持续运行:chan在常驻程序的不同goroutine之间传递信息
- 读写端,在同一程序运行:读写两端解耦但有关联,作为整体对外提供功能
- 适度缓存,做好丢失处理:作为内部信息传递通道,要考虑信息丢失的情况
引申探讨
无论是工程实践还是面试考察,以下引申话题都值得大家去探究。
- goroutine:要了解chan的原理以及灵活使用chan,就不可绕过对goroutine的深入理解
- select: 是chan常见的使用搭配,背后的机制值得分析
- context:context是工程中一定会用到的语法实现,它的背后有chan的支撑
mjlzz备注:文章供大家交流讨论,欢迎指正