当前位置: 首页 > news >正文

[Go]context上下文--使用要点--源码分析--Go核心--并发编程

参考:
https://learnku.com/docs/go-interviews/7-context/16574

intro:
Go最核心的就是routine并发编程。但是routine也具有一定的Metadata,并且执行会占用调度时间和CPU时间
在开过多routine后,可能会因为内存爆掉直接导致项目崩溃。
这时候就需要对自己所使用的routine进行管理。

由于routine按照树状模式展开,context本身的存在就是为了routine而存在的。所以context在使用过程中也会呈现出树状结构

这里先直接PO出Go 开发者的建议:

  1. 不要把context放在结构体里面,直接作为函数调用的第一个参数传入
  2. 不要传入nil,不知道传什么传递nil
  3. 不要把普通的函数参数放到context进行保存,context应该存储共同的数据,比如cookie等
  4. 同一个context可能会被传递到多个routine,但是context是并发安全的

源码开撕

第一个是context

type Context interface {// 当 context 被取消或者到了deadline,返回一个被关闭的 channelDone() <-chan struct{}// 在 channel Done 关闭后,返回 context 取消原因Err() error// 返回 context 是否会被取消以及自动取消时间(即 deadline)Deadline() (deadline time.Time, ok bool)// 获取 key 对应的 valueValue(key interface{}) interface{}
}

可以看到context本身是以接口方式存在的,这里就给了一定空间进行自定义。可以自己实现接口完成自己的context。

  • Done 返回一个chan,但是这个chan是一个只读的,所以一般只作为触发器,如果进行读入操作会导致当前routine挂起
  • Err 当时间到了,或者关闭,保存关闭原因
  • Deadline 表示这个context截止时间,可以根据这个时间,routine决定是否需要进行某些操作。
  • Value 获取之前设置key 的value

接下来是源码自带的emptyContext

type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return
}func (*emptyCtx) Done() <-chan struct{} {return nil
}func (*emptyCtx) Err() error {return nil
}func (*emptyCtx) Value(key interface{}) interface{} {return nil
}var (background = new(emptyCtx)todo       = new(emptyCtx)
)
func Background() Context {return background
}func TODO() Context {return todo
}

都实现了接口但都是朴实无华的nil
并且将这个empty起了两个名字,一个叫做background,一个叫做todo

  • background主要作为根context
  • TODO只是在不知道用什么context时使用,作为一个代码标注而已

接下来到了最重要的context:cancelContext
首先先两出两个cancel context的理念:

  • caller不应该过度干涉callee的情况,决定如何以及何时return应该由callee来决定。caller只能够给出建议需要关闭。
  • 取消操作可以进行树状传递
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}type cancelCtx struct {Context// 保护之后的字段mu       sync.Mutexdone     chan struct{}children map[canceler]struct{}err      error
}
func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}

其中第一个声明了canceler接口,只要具有Done和cancel函数就表示这是一个可以cancel的接口。换言之只要实现cancel(removeFromParent bool, err error)的context就一定是一个可cancel的context。因为context本身就需要实现Done。只是说提醒一下需要重写Done函数。
这两个函数大小写就印证了第一个理念,routine只能够Done表示这个context应该结束,而没有权利直接cancel掉,按照链式(在后面)也没有权利调用子context,只能够传递我认为应该需要cancel掉的信息。

同时也进行加锁操作,保证了并发的安全性。
children字段印证了第二个理念,cancel需要传递。

再来看看cancel函数

func (c *cancelCtx) cancel(removeFromParent bool, err error) {// 必须要传 errif err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // 已经被其他协程取消}// 给 err 字段赋值c.err = err// 关闭 channel,通知其他协程if c.done == nil {c.done = closedchan} else {close(c.done)}// 遍历它的所有子节点for child := range c.children {// 递归地取消所有子节点child.cancel(false, err)}// 将子节点置空c.children = nilc.mu.Unlock()if removeFromParent {// 从父节点中移除自己 removeChild(c.Context, c)}
}

其中最主要的事情:

  1. 判断是否被别的已经cancel了
  2. 遍历所有children,全部cancel
  3. 断绝与所有children的关系
  4. 根据字段判断是否需要断绝父子关系

而在中间遍历的child.cancel也解释了为什么这个方法需要一个字段评判。

  • 如果是父提出的cancel,那么就不需要断绝关系,因为本身父就需要将childrennil
  • 如果是孩子提出的cancel,这时候就需要断绝父子关系(如下面这个)
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}
var Canceled = errors.New("context canceled")

最后一个重要的函数
propagateCancel

func propagateCancel(parent Context, child canceler) {// 父节点是个空节点if parent.Done() == nil {return // parent is never canceled}// 找到可以取消的父 contextif p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// 父节点已经被取消了,本节点(子节点)也要取消child.cancel(false, p.err)} else {// 父节点未取消if p.children == nil {p.children = make(map[canceler]struct{})}// "挂到"父节点上p.children[child] = struct{}{}}p.mu.Unlock()} else {// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {for {switch c := parent.(type) {case *cancelCtx:return c, truecase *timerCtx:return &c.cancelCtx, truecase *valueCtx:parent = c.Contextdefault:return nil, false}}
}

可以看到上面生成的cancel还有一个问题是没有绑定父子关系,这个操作就由propagateCancel函数实现。

而最有意思的就是这个函数的最后几行,他有另外一个分支,假设说父routine不可取消(其实也就不是cancel类,只是empty或者是value类),那么就会开辟一个routine去监听,父与子的Done信号。

为什么需要这么做?
因为可能存在父是cancel,但是子并不是cancel,而子子又是cancel。
比如说父是cancel类别,子是KV context。这时候cancel里面的child不会保存子KV context,而子context会继承父cancel,此时这个子KV context又创建了一个cancel context。
这时候假设父需要cancel,父不会调用子子cancel方法,因为父cancel里面的children没有这个子子cancel。
但这个子子cancel会知道父cancel的Done信息,因为KV context是直接继承了父cancel,所以KV context的Done与父cancel的Done是同一个。
所以需要这个routine,来保证不会出现上述这个边界情况。

那么为什么有需要监听自己的Done呢?
因为如果自身都已经cancel了,就没必要去关心爷爷是否cancel了。跑这个routine只会占用系统资源。

以下是KV context,唯一一个需要注意的是,KV里面存储的不是线程安全的。所以一般只存放只读信息。

type valueCtx struct {Contextkey, val interface{}
}
func (c *valueCtx) String() string {return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}
func WithValue(parent Context, key, val interface{}) Context {if key == nil {panic("nil key")}if !reflect.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

最后一个就是timer

type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {// 直接调用 cancelCtx 的取消方法c.cancelCtx.cancel(false, err)if removeFromParent {// 从父节点中删除子节点removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {// 关掉定时器,这样,在deadline 到来时,不会再次取消c.timer.Stop()c.timer = nil}c.mu.Unlock()
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数return WithCancel(parent)}// 构建 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  deadline,}// 挂靠到父节点上propagateCancel(parent, c)// 计算当前距离 deadline 的时间d := time.Until(deadline)if d <= 0 {// 直接取消c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(true, Canceled) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {// d 时间后,timer 会自动调用 cancel 函数。自动取消c.timer = time.AfterFunc(d, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}

需要注意的:

  1. time类别本身也是一个cancel
  2. 但imer也可以以非cancel作为父,因为调用的是newCalcelCtx(parent)
http://www.xdnf.cn/news/918793.html

相关文章:

  • go-zero微服务入门案例
  • 【Go语言基础【13】】函数、闭包、方法
  • 优化器 (torch.optim) 与学习率调度器 (lr_scheduler)
  • vite+tailwind封装组件库
  • Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
  • Xela矩阵三轴触觉传感器的工作原理解析与应用场景
  • 一.设计模式的基本概念
  • Python分形几何可视化—— 复数迭代、L系统与生物分形模拟
  • Redis专题-基础篇
  • Vue具名插槽
  • Linux(13)——Ext系列文件系统
  • Now formdata是什么?如何使用
  • RT-Thread内核组成——内核移植
  • MySQL(61)如何进行数据库分区?
  • 锁的艺术:深入浅出讲解乐观锁与悲观锁
  • 计算机操作系统(十五)死锁的概念与死锁的处理方法
  • 【高效开发工具系列】Blackmagic Disk Speed Test for Mac:专业硬盘测速工具
  • Qt6.8编译MySQL
  • Fullstack 面试复习笔记:HTML / CSS 基础梳理
  • 【物联网-ModBus-ASCII】
  • vue3项目怎么适配不同尺寸的屏幕?
  • 计算机组成与体系结构:补码数制二(Complementary Number Systems)
  • FFmpeg 实现 100 台设备同屏的高效码流压缩
  • Python-进程
  • Playwright自动化测试全栈指南:从基础到企业级实践(2025终极版)
  • 柯尼卡美能达Konica Minolta bizhub 205i打印机信息
  • 线程池封装
  • ubuntu 22.04虚拟机配置静态IP
  • springBoot 通过模板导出Excel文档的实现
  • 几种简单的排序算法(C语言)