Go channel
Go语言中的Channel是什么,有哪些用途,如何处理阻塞
Channel是用于在goroutines之间进行通信的一种机制。通道提供了一种并发安全的方式来进行goroutines之间通信。通过通道可以避免在多个goroutines之间共享内存而引发的竞态条件问题,因为通道的读写是原子性的。
用途
• 数据传递: 主要用于在goroutines之间传递数据,确保数据的安全传递和同步。
• 同步执行: 通过Channel可以实现在不同goroutines之间的同步执行,确保某个goroutine在另一个goroutine完成某个操作之前等待。
• 消息传递: 适用于实现发布-订阅模型或通过消息进行事件通知的场景。
• 多路复用: 使用 select 语句,可以在多个Channel操作中选择一个非阻塞的执行,实现多路复用。
如何处理阻塞
- 缓冲通道,在创建通道时指定缓冲区大小,即创建一个缓冲通道。当缓冲区未满时,发送数据不会阻塞。当缓冲区未空时,接收数据不会阻塞。
- select语句用于处理多个通道操作,可以用于避免阻塞。
- 使用time.After 创建一个定时器,可以在超时后执行特定的操作,避免永久阻塞。
- select 语句中使用 default 分支,可以在所有通道都阻塞的情况下执行非阻塞的操作。
channel的底层结构
type hchan struct {
qcount uint //Channel 中的元素个数
dataqsiz uint //Channel 中的循环队列的长度
buf unsafe.Pointer //Channel 的缓冲区数据指针
elemsize uint16
closed uint32
elemtype *_type
sendx uint //Channel 的发送操作处理到的位置
recvx uint //Channel 的接收操作处理到的位置
recvq waitq
sendq waitq
lock mutex
}
结构体中的五个字段 qcount、dataqsiz、buf、sendx、recv 构建底层的循环队列
elemsize 和 elemtype 分别表示当前 Channel 能够收发的元素类型和大小
sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine列表。这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构
type waitq struct {
first *sudog
last *sudog
}
channel的关闭
- 关闭未初始化过的 channel 会 panic;
- 加锁;
- 重复关闭 channel 会 panic;
- 将阻塞读协程队列中的协程节点统一添加到 glist;
- 将阻塞写协程队列中的协程节点统一添加到 glist;
- 唤醒 glist 当中的所有协程.
channel的发送和接收操作有哪些基本特性?
1.对同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
即同一时刻,go的runtime只会执行对同一个通道的任意个发送操作中的某一个,直到这个发送的元素值被完全复制进该通道之后,其他发送操作才可能被执行。接收操作类似。
2.发送操作和接收操作中对元素值的处理都是不可分割的
发送操作和接收操作都是原子的。例如接收操作时元素值从通道移动到外界,这个移动操作包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值,这两个操作会一起完成。 既是为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性
3.发送操作和接收操作在完全完成之前会被阻塞
发送操作也是包括了复制元素值和放置副本到通道内部两个步骤。在这两个步骤完全完成之前,发起这个操作的那句代码会一直阻塞在那里,在通道完成发送操作之后,runtime系统会通知这句代码所在的goroutine, 解除阻塞,以使它去争取继续运行代码的机会。
读写channel什么时候出现问题
对一个已经关闭的通道进行写操作(发送)会引发panic
关闭一个nil(没有初始化的channel)会引发panic
对一个已经关闭的通道进行关闭操作会引发panic
对一个nil(没有初始化的channel)进行读操作和写操作会阻塞
对已经关闭的通道可以进行接收操作,通道中没有元素的时候返回通道的零值
无缓冲的 channel 和有缓冲的 channel 的区别?
对于无缓冲区channel: 发送的数据如果没有被接收方接收,那么发送方阻塞;如果一直接收不到发送方的数据,接收方阻塞;
有缓冲的channel:发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。
select实现机制
1.锁定scase中所有channel
2.按照随机顺序检测scase中的channel是否ready
可读就读,可写就写,没准备好就继续检测(关闭的channel也可以读取)
3.所有case都没有准备好,而且没有default
(1)将当前的goroutine加入到所有channel的等待队列 (2)将当前协程转入阻塞,等待被唤醒
4.唤醒之后,返回channel对应的case index
Golang context实现原理
context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制。除此之外,context 还兼有一定的数据存储能力。
context.Context 为 interface,定义了四个核心 api
type Context interface {
Deadline() (deadline time.Time, ok bool) //返回 context 的过期时间
Done() <-chan struct{} //返回用以标识ctx是否结束的chan
Err() error //返回ctx错误
Value(key any) any //返回ctx存放的对应于key的Value
}
标准error主要分为两类
//被主动取消
var Canceled = errors.New("context canceled")
//过期时间达到
var DeadlineExceeded error = deadlineExceededError{}
emptyctx
//emptyctx的结构
type emptyCtx int
//返回deadline time.Time公元元年时间,以及false的flag表示当前 context不存在过期时间
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} { //返回一个空的chan (读写空chan会阻塞)
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return
}
context.Background()和context.TODO()返回的均是 emptyCtx 类型的一个实例
cancelCtx
type cancelCtx struct {
Context //内嵌一个context,指向唯一的父节点
mu sync.Mutex //用以协调并发场景下的资源获取
done atomic.Value //chan struct{},用以反映cancelCtx生命周期的通道
children map[canceler]struct{} //指向 cancelCtx 的所有子 context
err error //当前 cancelCtx 的错误
}
//只关心孩子的cancel和done的信息,别的信息不关注(职责内聚,边界分明)
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
Deadline方法:cancelCtx未实现Deadline 方法,仅是embed了一个带有 Deadline 方法的Context interface,如果直接调用会报错。
Done方法:
func (c *cancelCtx) Done() <-chan struct{} {
//基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
//加锁后,在此检查 chan 是否存在,若存在则返回;(double check)
c.mu.Lock()
defer c.mu.Unlock()
//初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
Err 方法
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err //读取 cancelCtx.err
c.mu.Unlock()
return err
}
Value 方法
func (c *cancelCtx) Value(key any) any {
//倘若 key 特定值 &cancelCtxKey(一个全局的值),则返回 cancelCtx 自身的指针
if key == &cancelCtxKey {
return c
}
//否则遵循 valueCtx 的思路取值返回
return value(c.Context, key)
}
context.WithCancel()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
//校验父 context 非空
if parent == nil {
panic("cannot create context from nil parent")
}
//注入父 context 构造好一个新的 cancelCtx
c := newCancelCtx(parent)
//启动一个守护协程,以保证父 context 终止时,该 cancelCtx 也会被终止
propagateCancel(parent, &c)
//将 cancelCtx 返回,连带返回一个用以终止该cancelCtx 的闭包函数
return &c, func() { c.cancel(true, Canceled) }
}
newCancelCtx()
func newCancelCtx(parent Context) cancelCtx {
//注入父 context 后,返回一个新的 cancelCtx
return cancelCtx{Context: parent}
}
propagateCancel() :保证父context取消,子context也会取消。用以传递父子 context 之间的 cancel 事件
如果parent是不会被cancel的类型(如emptyCtx),则直接返回. 如果parent已经被cancel,则直接终止子context,并以parent的err作为子context的err; 如果parent是cancelCtx的类型,则加锁,并将子context添加到parent的children map当中,这样在父context结束的时候也会顺带将所有的子context结束。 如果parent不是cancelCtx类型,但又存在cancel的能力( 比如用户自定义实现的context),则启动一个守护协程,通过select多路复用的方式,监控两个channel,parent.Done()和child.Done()。如果parent终止,则同时终止子context,并透传parent的err。如果子context终止则不需要操作。
怎么判断context是否为cancelCtx?
cancelCtx中的value方法中有一个全局值cancelCtxKey,基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议。可以以此判断context是否为cancelCtx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
cancelCtx.cancel()
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
//首先校验传入的 err 是否为空,若为空则 panic
if err == nil {panic("context: internal error: missing cancel error")}
c.mu.Lock()
//校验 cancelCtx 自带的 err 是否已经非空,若非空说明已被 cancel,则解锁返回
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
//若channel此前未初始化,则直接注入一个closedChan,否则关闭该 channel
//closechan用于标识当前channel已经关闭
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
//遍历当前 cancelCtx 的 children set,依次将 children context 都进行 cancel
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
//把cancelCtx从parent的children set 中移除
if removeFromParent {
removeChild(c.Context, c)
}
}
对context进行取消,需要在当前context存入err 保证当前的context的channel能够被上游的用户读取到信号,不再会阻塞。 将同样是cancelctx的孩子统统执行cancel操作
timerCtx
在 cancelCtx 基础上又做了一层封装,新增了一个 time.Timer 用于定时终止 context,另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间。
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
timerCtx.Deadline()
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
timerCtx.cancel()
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 复用继承的 cancelCtx 的 cancel 能力,进行 cancel 处理
c.cancelCtx.cancel(false, err)
// 判断是否需要手动从 parent 的 children set 中移除,若是则进行处理
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
//停止 time.Timer,回收该资源
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
创建timectx
WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) :设置相对时间
WithDeadline(parent Context, d time.Time) (Context, CancelFunc):设置绝对时间
校验 parent context 非空;校验 parent 的过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;构造出一个新的 timerCtx;启动守护方法,同步 parent 的 cancel 事件到子 context;判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;启动 time.Timer,设定一个延时时间,即达到过期时间后会终止该 timerCtx,并返回 DeadlineExceeded 的错误;返回 timerCtx,已经一个封装了 cancel 逻辑的闭包 cancel 函数
valueCtx
type valueCtx struct {
Context //继承了一个 parent context
key, val any //一个 valueCtx 中仅有一组 kv 对
}
func (c *valueCtx) Value(key any) any {
//假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value
if c.key == key {
return c.val
}
return value(c.Context, key)
}
value(c Context, key any) any方法启动一个 for 循环,由下而上,由子及父,依次对 key 进行匹配,其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式,到匹配的 key,则将该组 value 进行返回。
context.WithValue()方法
parent context 为空,panic;倘若 key 为空 panic;倘若 key 的类型不可比较,panic;包括 parent context 以及 kv对,返回一个新的 valueCtx
valueCtx 不适合视为存储介质,存放大量的 kv 数据
- 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;
- 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);
- 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v. 由此得知,valueContext 的定位类似于请求头,只适合存放少量作用域较大的全局 meta 数据.