Go语言 context的源码分析与典型使用(WithCancel,WithTimeout,WithDeadline,WithValue)

2023-11-12

context原理

context 主要用来在 goroutine 之间传递上下文信息,包括取消信号(WithCancel)、超时时间(WithTimeout)、截止时间(WithDeadline)、键值对key-value(WithValue)
在这里插入图片描述

Context基本结构

源码中Context接口如下:

type Context interface {
	Deadline() (deadline time.Time, ok bool)   //超时设置
	Done() <-chan struct{}			//接收取消信号的管道,对应cancel
	Err() error                 
	Value(key interface{}) interface{}   //上下文键值对
}

WithCancelWithTimeoutWithValue等函数返回的都是实现了这些方法的对应结构体,如图所示
在这里插入图片描述

WithCancel

协程A中初始化ctx代码:

ctx1, cancel := context.WithCancel(context.Background())

将ctx传给子协程,并监听管道信号:

select {
		case <-ctx.Done():
			fmt.Println("goroutine recv")
			return
		default:
			time.Sleep(time.Second * 1)
		}

如图,在A协程中调用cancel()函数将会关闭所有子协程(B,C,D,E,F,G),下面详细分析这一原理。
首先看下WithCancel的实现:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)   //返回一个cancelCtx结构体
	propagateCancel(parent, &c)   //绑定父子context关系
	return &c, func() { c.cancel(true, Canceled) }
}

go源码中cancelCtx的结构,Context 是自己的父context,done就是接收取消信号的管道,children 包含了自己路径下的所有子context集合。

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
}

ctx.Done()其实就是返回一个管道,也就是上面cancelCtx 结构体中的done

func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}

再来看看什么时候往这个管道中发送取消信号,分析cancelCtxcancel方法,可以发现取消信号就是关闭cancelCtxdone管道,因此只要父协程触发cancel,所有子协程中监听的ctx.Done()就会收到关闭管道的信号进入可读状态,从而执行selct下面的case语句。

var closedchan = make(chan struct{})
func init() {
	close(closedchan)   //预先初始化一个已经关闭的管道,下面cancel函数会用到
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	//重点在这,取消信号其实就是关闭通道,结合上面的Done方法来理解
	if c.done == nil {
		c.done = closedchan    //如果在子协程的Done()方法调用之前就调用了父协程的cancel函数,
							  //c.done还没赋值,将其置为一个关闭管道,子协程接收端收到关闭信号
	} else {
		close(c.done)         //如果子协程调用Done()方法之后再调用父协程的cancel函数,就直接关闭管道
		                     //ps:看出来两次调用cancel函数就会关闭已经关闭的管道,触发panic
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)   //所有子context发送取消信号
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

这段代码注意体会这个预先初始化且已经关闭的管道closedchan。

WithTimeout,WithDeadline

WithTimeout底层实现是调用的WithDeadline,所以只需关注WithDeadline的代码实现

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

分析WithDeadline源代码,其实就是添加了timer计时器,time.AfterFunc中计时条件满足时自动触发cancel函数

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)  //绑定父子context关系
	dur := time.Until(d)
	if dur <= 0 {           //时间参数检查
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {   //重点在这,绑定dur和c.cancel函数
			c.cancel(true, DeadlineExceeded)     //时间满足后自动触发
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

WithValue

WithValue的源码更为简单,返回一个带有键值对的valueCtx结构体

func WithValue(parent Context, key, val interface{}) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

valueCtx结构体表示如下:

type valueCtx struct {
	Context
	key, val interface{}
}

可以发现每个valueCtx结构体只能存一对键值对,但有趣的是,在查找value过程中会向自己的父contex递归查找,Value方法源码如下:

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val    //如果找到直接返回value值
	}
	return c.Context.Value(key)   //如果没找到直接向上递归查找
}

递归总要有个头,当查找到最初的父context,即初始化时由Background()得到的emptyCtx时就会结束

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

代码实现

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)
var wg sync.WaitGroup
type contextString string

func go_withCancel(ctx context.Context) {
	defer wg.Done()
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("go_withCancel recv 1s")
			break LABEL
		default:
			time.Sleep(time.Second * 1)
		}
	}
}

func go_withTimeOut(ctx context.Context) {
	defer wg.Done()
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("go_withTimeOut recv 2s")
			break LABEL
		default:
			time.Sleep(time.Second * 1)
		}
	}
}

func go_WithDeadline(ctx context.Context) {
	defer wg.Done()
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("go_WithDeadline recv 3s")
			break LABEL
		default:
			time.Sleep(time.Second * 1)
		}
	}
}

func go_WithValue(ctx context.Context) {
	defer wg.Done()
	fmt.Println("go_WithValue get go_value = ", ctx.Value(contextString("go_value")))
}
func main() {
	ctx1, cancel1 := context.WithCancel(context.Background())
	ctx2, _ := context.WithTimeout(context.Background(), time.Second*2)                  //2s后自动结束
	ctx3, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3)) //3s后自动结束
	ctx4 := context.WithValue(context.Background(), contextString("go_value"), "hello")  //设置上下文键值对
	wg.Add(4)
	go go_withCancel(ctx1)
	go go_withTimeOut(ctx2)
	go go_WithDeadline(ctx3)
	go go_WithValue(ctx4)
	time.Sleep(time.Second * 1)
	cancel1() //1s后调用cancel结束go_withCancel协程
	wg.Wait()
}

测试结果如下:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go语言 context的源码分析与典型使用(WithCancel,WithTimeout,WithDeadline,WithValue) 的相关文章

随机推荐

  • IDEA 2020.1汉化问题解决办法

    IDEA2020 1 如何汉化 百度网盘链接 汉化版本 zh 201 6668 113 链接 https pan baidu com s 12Fq5QqgeRRdu6 2XIu37tA 提取码 Dl12 安装步骤 第一种 如果插件中心可以直
  • Python 的数据可视化之常用的那些图的简单示例

    参考网站 https pyecharts org zh cn intro 柱形图 折线图 饼图 词云 动态散点图 关系图 散点图 树图 1 柱形图 rom pyecharts charts import Bar from pyecharts
  • 关于毛刺

    关于毛刺 http blog csdn net guqian110 article details 17304065 目录 1 毛刺的产生原因 冒险和竞争 使用分立元件设计电路时 由于PCB在走线时 存在分布电容和电容 所以在几ns内毛刺被
  • colorui radio 样式_uni-app 修改 radio/checkbox/switch 组件样式

    我们在用 uni app 开发前端页面时 往往需要修改一些组件的默认样式 特别是颜色 那么如何修改 radio checkbox switch 单选框 复选框 开关 的样式呢 可以通过覆盖样式的方式实现 下面以单选框 radio 为例 基于
  • qt学习笔记5:资源文件的添加、模态和非模态对话框创建

    这次创建的时候勾选ui创建界面 打开 ui文件 在这里就可以通过拖拽的方式对内容进行实现 可以在 ui中进行简单界面设计一些拖拽生成需要的控件 然后在 cpp中进行代码实现 添加资源 比如要添加图片 首先将资源文件放到文件项目中 可以通过右
  • 二 Qt Remote Objects (REPC 编译器)

    REPC 概述 Replica Compiler repc 基于 API 定义文件生成QObject头文件 该文件 称为 rep 文件 使用特定的 文本 语法来描述 API 文件扩展名为 rep 是 Replica 的缩写 当这些文件被re
  • 语义分割论文-DeepLab系列

    语义分割论文 DeepLab系列 DeepLabv1 收录 ICLR 2015 International Conference on Learning Representations 代码 github Caffe Semantic im
  • CDN防御与高防服务器有什么区别

    CDN防御与高防服务器的区别 1 本质不同 高防服务器是指独立单个硬防防御50G以上的服务 而CDN防御是指通过在现有的Internet中增加一层新的网络架构 2 两者的防御方式不同 高防服务器采用单机防御或者集群防御 而CDN采用多节点分
  • VTD安装教程

    VTD安装 1 安装Nvidia显卡驱动 2 安装依赖包 3 安装license管理工具 helium 4 安装VTD软件 4 1 安装包内容 4 2 安装流程 5 配置license 运行软件 6 配置license管理工具开机自启动 1
  • 如何使用postman做接口测试

    常用的接口测试工具主要有以下几种 Postman 简单方便的接口调试工具 便于分享和协作 具有接口调试 接口集管理 环境配置 参数化 断言 批量执行 录制接口 Mock Server 接口文档 接口监控等功能 JMeter 开源接口测试及压
  • DropDownList 绑定数据

    如何使用DropDownList 控件绑定数据呢 今天我们来介绍一下比较常用的一种方法 前后台结合方式 首先 我们需要拉一个DropDownList 控件 然后 通过控件配置SqlDataSource数据源 选择合适的数据表 接着 设置Da
  • vatic视频标注工具的安装使用及错误解决

    1 安装 基于Ubuntu16 04 sudo pip install cython 0 20 wget http mit edu vondrick vatic vatic install sh 注 vatic install sh可能下载
  • 如何使用java连接MySQL数据库

    如何使用java连接MySQL数据库 编者 芊默 使用的MySQL版本 8 0 19 序言 因为数据库厂商有很多种 为了能够统一让java程序员使用更方便 sun公司编写了一套JDBC接口用于连接数据库 但是只有JDBC接口我们仍然是连接不
  • 聊一聊,这些年我用Python爬虫挣钱的那些事

    1 最典型的就是找爬虫外包活儿 这个真是体力活 最早是在国外各个freelancer网站上找适合个人做的小项目 看见了就赶紧去bid一下 把价格标得死死的 由于是挣dollar 当时换算成人民币是1 7 5左右感觉还是换算 别人标几百刀 我
  • OpenGL:gl_ClipDistance和gl_CullDiatance

    学到了OpenGL中的用户裁剪和前剪切 记录下gl ClipDistance和gl CullDiatance的用法 gl ClipDiatance 输出的裁剪距离将和图元进行线性插值 插值距离小于0 则图元部分将剪切掉 gl CullDia
  • Python数据预处理

    学 目录 1 数据表的基本信息查看 2 查看数据表的大小 3 数据格式的查看 4 查看具体的数据分布 二 缺失值处理 1 缺失值检查 2 缺失值删除 3 缺失值替换 填充 三 重复值处理 1 发现重复值 四 异常值的检测与处理 1 检测异常
  • Linux错误 ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

    ERROR 1045 28000 Access denied for user root localhost using password YES 翻译 错误1045 28000 对用户 root 本地主机拒绝访问 使用密码 是 一般这个错
  • Java String总结

    文章目录 创建String 字符串比较 字符串常量池 直接赋值 构造方法 理解字符串不可变 反射 特殊手段 char 和String StringBuffer和StringBuilder String API 创建String 常见的构造方
  • 实验五_linux进程控制,实验五Linux进程管理

    实验五 Linux进程管理 一 实验目的 1 进行系统进程管理 2 周期性任务安排 二 实验内容 1 进程状态查看 2 控制系统中运行的进程 3 安排一次性和周期性自动执行的后台进程 三 实验练习 任务一 进程管理 实验内容 1 查看系统中
  • Go语言 context的源码分析与典型使用(WithCancel,WithTimeout,WithDeadline,WithValue)

    文章目录 context原理 Context基本结构 WithCancel WithTimeout WithDeadline WithValue 代码实现 context原理 context 主要用来在 goroutine 之间传递上下文信