深入理解 go context

news2024/11/25 10:35:16

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!

image

原文链接:深入理解 go context 欢迎点赞关注

context 介绍

context 是 Go 语言中用于处理并发操作的一个重要概念。context也被称作上下文,主要用于在 goroutine 之间传递截止日期、取消信号和其他请求范围的值。

什么是 Go Context

Context 是一个接口,它定义了以下方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

  1. Deadline(): 返回与 Context 关联的截止时间。如果没有设置截止时间,则返回 zero time.Time 和 false。
  2. Done(): 返回一个 channel,当 Context 被取消或者截止时间到达时,该 channel 会被关闭。
  3. Err(): 返回 Context 被取消的原因。如果 Context 还未被取消,则返回 nil。
  4. Value(key interface{}): 返回与 Context 关联的请求范围的值。

context 的作用

Go Context 主要用于以下几个方面:

  • 传递取消信号: 当一个长时间运行的操作被取消时,它可以及时停止并释放资源。
  • 设置截止时间: 当一个操作超过预期时间时,可以自动取消该操作,避免阻塞。
  • 携带请求范围的数据: 可以在 goroutine 之间传递一些请求相关的数据,如用户 ID、跟踪 ID 等。

Context底层实现

context 树状模型

go 提供了四种创建context的函数:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

可以看到在构建过程中都是基于 parent context 来构建子 context,子 context 还可以继续派生新的子 context 因此 context 是一个类似树状的模型:

context 的根节点

context 树的最原始的根节点通常是 context.Background()context.TODO(),他们底层都是基于 emptyCtx 实现的:

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}


func TODO() Context {
    return todo
}

emptyCtx 的实现也特别简单,只是对 Context 接口的实现,逻辑比较简单这里就不赘述了:

type emptyCtx int

func (*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 any) any {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

这里有个问题:context.Background()context.TODO() 底层实现是一样的,那么为什么暴露两个函数给用户呢?

原因是它们拥有不同的语义

  • context.Background()

Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

  • context.TODO()

TODO returns a non-nil, empty Context. Code should use context.TODO when it’s unclear which Context to use or it is not yet available (because the
surrounding function has not yet been extended to accept a Context parameter).

context.WithValue

WithValue 实际会返回 valueCtx类型:

func WithValue(parent Context, key, val any) 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}
}

type valueCtx struct {
	Context
	key, val any
}

valueCtx类型和链表的节点比较像,通过Context字段指向父节点,key 和 val 存储 valueCtx 的 key 、val 参数。

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "a", 100)
    ctx = context.WithValue(ctx, "b", 200)
    ctx = context.WithValue(ctx, "c", 300)
    
}

上面的代码执行后会形成类似下面的链表:

这里需要注意的:

  1. key 必须是可以比较的类型,推荐自定义一个 struct{} 类型
  2. WithValue 的结果是一个链表,查找复杂度是 O ( n ) O(n) O(n) 所以不要使用WithValue传递大量的key-val。

key-val 查找是通过 valueCtx.Value() 实现的,整个查找过程就是沿着链表的最后一个节点一个一个向上查找:

type valueCtx struct {
    Context
    key, val any
}


func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &cancelCtxKey {
                return c
            }
            c = ctx.Context
        case *timerCtx:
            if key == &cancelCtxKey {
                return ctx.cancelCtx
            }
            c = ctx.Context
        case *emptyCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}

context.WithCancel

context.WithCancel 的底层实现是 withCancel 函数,withCancel 函数主要有两个功能:

  1. 调用 newCancelCtx 创建 cancelCtx 类型的 ctx 实例
  2. 调用 propagateCancel 将 ctx 实例挂载到父节点上
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := withCancel(parent)
    return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)  // 创建 cancelCtx 实例
    propagateCancel(parent, c) // 把当前节点
    return c
}

func newCancelCtx(parent Context) *cancelCtx {
  return &cancelCtx{Context: parent}
}
cancelCtx 类
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of 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
    cause    error                 // set to non-nil by the first cancel call
}

字段解释:

  • mu:就是一个互斥锁,保证并发安全的,所以context是并发安全的
  • done:用来做context的取消通知信号,之前的版本使用的是chan struct{}类型,现在用atomic.Value做锁优化
  • children:key是接口类型canceler,目的就是存储实现当前canceler接口的子节点,当根节点发生取消时,遍历子节点发送取消信号
  • error:当context取消时存储取消信息
propagateCancel 函数
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err(), Cause(parent))
        return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err, p.cause)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        goroutines.Add(1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err(), Cause(parent))
            case <-child.Done():
            }
        }()
    }
}

propagateCancel 函数的逻辑也比较简单:

  1. 调用 parentCancelCtx 寻找可取消的父节点
  2. 如果找到了就把当前节点加入到父节点的 children 里面
  3. 如果没找到则需要起一个协程来监听父节点和当前节点的取消事件
  4. 挂载的目的是父节点取消是当前节点也能被取消

这里有个问题为什么没找到父节点,还要监听父节点的取消事件呢?原因是 parentCancelCtx 函数只能识别 *cancelCtx 类型的父节点,如果父节点是实现了 Context 类型的自定义类型或者是嵌套了*cancelCtx 就识别不出来,所以需要启动一个协程来监听自定类型或者嵌套类型的取消事件。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nil, false
    }
    return p, true
}

parentCancelCtx函数是寻找一个父节点,关键逻辑 cancelCtxKey + valuea()

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &cancelCtxKey {
                return c
            }
            c = ctx.Context
        case *timerCtx:
            if key == &cancelCtxKey {
                return ctx.cancelCtx
            }
            c = ctx.Context
        case *emptyCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}

可以看到当 key == &cancelCtxKey 时:

  1. cancelCtx类型返回的自身
  2. timerCtx类型返回的 timerCtx.cancelCtx
cancel 函数

cancelCtx.cancel 是取消 ctx 的具体实现

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

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

cancel 函数的实现也比较简单:

  1. 关闭 done channel
  2. 调用所有子节点的cancel函数,取消所有子节点
  3. 根据 removeFromParent 参数来决定是否要从删除父节点删除当前节点
    • 因为父节点取消而被动取消的情况 removeFromParent 为false
    • 当前节点主动取消的情况 removeFromParent 为 true

context.WithDeadline

context.WithDeadline 的逻辑也同样比较简单:

  1. 创建一个 timerCtx 类型的实例并返回
  2. 将 timerCtx 实例挂载到父节点上
  3. 启动一个定时器,定时调用 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)
    }
    
    // 创建一个 timerCtx 类型的实例并返回
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    
    // 将 timerCtx 实例挂载到父节点上
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
        return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        // 启动一个定时器,定时调用 cancel 方法
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, nil)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}
timerCtx 类型
type timerCtx struct {
    *cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
    c.cancelCtx.cancel(false, err, cause)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

  1. timerCtx 内嵌了 cancelCtx 相当于 timerCtx 继承了 cancelCtx
  2. 其中 timer 字段是实现WithDeadline、WithTimeout 的关键,其原理就是启动一个定时器定时调用 cancel 方法;
  3. timer 字段是非并发安全的,所以对timer的操作需要先加锁;

context.WithTimeout

context.WithTimeout 是基于 context.WithDeadline 实现的,这里就不赘述了。

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

小结

上面介绍了 context 的实现原理,里面涉及了很多接口与结构体,下面通过类图串一下他们之间的关系:

Context 的使用

取消信号传递

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker received cancellation signal.")
            return
        default:
            // Simulate some work
            time.Sleep(500 * time.Millisecond)
            fmt.Println("Working...")
        }
    }
}

func main() {
    parentCtx, cancel := context.WithCancel(context.Background())
    go worker(parentCtx)

    // Simulate main program execution
    time.Sleep(2 * time.Second)

    // Cancel the context to stop the worker
    cancel()

    // Wait for the worker to finish
    time.Sleep(1 * time.Second)
}

超时控制

package main

import (
    "context"
    "fmt"
    "time"
)

func operationWithTimeout(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second): // Simulate some long operation
        fmt.Println("Operation completed.")
    case <-ctx.Done():
        fmt.Println("Operation canceled due to timeout.")
    }
}

func main() {
    timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    operationWithTimeout(timeoutCtx)
}

截止时间

package main

import (
    "context"
    "fmt"
    "time"
)

func operationWithDeadline(ctx context.Context) {
    deadline, ok := ctx.Deadline()
    if ok {
        fmt.Printf("Operation must be completed before: %s\n", deadline)
    } else {
        fmt.Println("No specific deadline for the operation.")
    }

    // Simulate some operation
    time.Sleep(2 * time.Second)

    select {
    case <-ctx.Done():
        fmt.Println("Operation canceled due to context deadline.")
    default:
        fmt.Println("Operation completed within the deadline.")
    }
}

func main() {
    deadline := time.Now().Add(5 * time.Second)
    deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    operationWithDeadline(deadlineCtx)
}

请求范围的值传递

package main

import (
    "context"
    "fmt"
    "sync"
)

func processRequest(ctx context.Context, requestID int) {
    // Accessing request-scoped value from the context
    userID, ok := ctx.Value("userID").(int)
    if !ok {
        fmt.Println("Failed to get userID from context.")
        return
    }

    fmt.Printf("Processing request %d for user %d\n", requestID, userID)
}

func main() {
    // Creating a parent context with a request-scoped value
    parentCtx := context.WithValue(context.Background(), "userID", 123)

    var wg sync.WaitGroup

    // Simulating multiple requests
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(requestID int) {
            // Creating a child context for each request
            childCtx := context.WithValue(parentCtx, "requestID", requestID)
            processRequest(childCtx, requestID)
            wg.Done()
        }(i)
    }

    wg.Wait()
}

本文由mdnice多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1962747.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue基础知识总结(2)--- axios的使用

一.下载Vue3&#xff1a; 选择自己想要下载的项目文件夹&#xff0c;cmd回车打开命令栏&#xff0c;执行 &#xff1a; cnpm init vuelatest 然后等待一会就可以创建一个项目&#xff0c;并更改项目名&#xff1a; √ 请输入项目名称&#xff1a; ... vue-project 之后按照…

python做简单爬虫的一些常用组件

文章目录 前言requestjsonbs4 前言 最近一直在做零散的一次性的爬虫工作&#xff0c;基本都是用python开发的&#xff0c;整理一下python做小规模爬虫开发常用的一些工具类 request python最简单的发http请求的包&#xff0c;request.get和request.post就可以搞定绝大部分的…

ECMA6Script学习笔记(五)

【摘要】 本文是对自己学习ES6的学习笔记回顾,后面是概要: 本文介绍了ES6中的对象创建和拷贝方法。对象创建方面&#xff0c;ES6通过class关键字支持了面向对象的语法糖&#xff0c;包括属性设置、getter和setter方法、构造器、静态方法以及私有属性的定义。同时&#xff0c;展…

Linux系统root账号密码破解(重置)

如果想不起root账号密码应该怎么做&#xff1f; 先关机再重新启动&#xff08;重启虚拟机&#xff09;做相关操作&#xff0c;开机时不能让服务器完全启动&#xff0c;需要把开机引导系统&#xff08;GRUB&#xff09;调出来 注&#xff1a;在实际生产环境中使用的Linux没有图…

四,系统规划

一&#xff0c;企业系统规划法BSP&#xff08;2009、2010、2018&#xff0c;步骤以及优缺点、用到的工具&#xff09; 企业系统规划法&#xff08;Business Systems Planning&#xff0c;BSP&#xff09;是IBM公司20世纪70年代提出的一种结构化的信息系统规划方法。该方法通过…

Spring Cloud微服务项目公共类抽取

在微服务架构中&#xff0c;代码的重用性和维护性是非常重要的。Spring Cloud 提供了丰富的工具和框架来帮助我们构建和管理微服务应用&#xff0c;但随着项目规模的扩大&#xff0c;我们会遇到大量的重复代码和相似的逻辑。在这种情况下&#xff0c;抽取公共类成为提高代码质量…

回归测试:保障软件质量的利器

目录 前言1. 回归测试的概念1.1 定义1.2 目标 2. 回归测试的主要作用2.1 确保系统稳定性2.2 提高软件质量2.3 节省维护成本 3. 回归测试在整个测试中的地位3.1 单元测试阶段3.2 集成测试阶段3.3 系统测试阶段3.4 验收测试阶段 4. 回归测试的主要工具4.1 Selenium4.2 JUnit4.3 J…

《Redis设计与实现》读书笔记-数据结构(SDS)

目录 SDS定义 SDS结构 SDS与C字符串结构差异 SDS优点 SDS扩容策略 SDS惰性空间回收 SDS定义 SDS&#xff08;简单动态字符串&#xff09;&#xff0c;用于代替C语言自身的字符串&#xff08;字符容量与字符数组强相关&#xff09;。 SDS结构 sdshdr{int free //sds 中…

预警系统最小例程构建

预警系统最小例程构建 引言 为了更直观, 我们使用最小例程来实现这个预警流程, 环境温湿度读取,然后判断阈值, 超标则触发小灯警报。 最小例程工程备份链接: https://ww0.lanzoul.com/iz4wd261k21i 仿真文件工程备份链接: https://ww0.lanzoul.com/i8vTn261syyb 文章目录…

Jeecgboot仪表盘设计器使用https时访问报错

问题 仪表盘设计器设计好后&#xff0c;Nginx配置域名发送https请求时&#xff0c;/drag/page/queryById、/drag/page/addVisitsNumber仍发送http请求。导致发送下面错误&#xff1a; 原因 仪表盘设计器里设计的页面是由后端生成返回给前端的&#xff0c;后端是根据后端服…

docker安装elasticsearch(es)最新版本

docker安装elasticsearch&#xff08;es&#xff09; docker官网 https://hub.docker.com/ https://www.cnblogs.com/balloon72/p/13177872.html 1、拉取最新项目elasticsearch docker pull elasticsearch:8.14.3lscpu 查看架构 2、构建环境 mkdir -p /data/elasticsear…

【TOOLS】Project 2 Maven Central

发布自己的项目到maven中央仓库 Maven Central Account 访问&#xff1a;https://central.sonatype.com/&#xff0c;点击右上角&#xff0c;根据提示注册账号 构建User token &#xff0c;用于访问中央仓库的API&#xff1a; 点击右上角&#xff0c;查看账户点击Generate Us…

医院体检信息管理系统,C#体检系统源码,健康体检系统PEIS

体检服务全流程 检前 检前注意事项提醒-体检预约-套餐选择-体检签到-费用缴纳 检中 科室队列提醒-增项检中支付 检后 报告查询-体检百科-报告解读-问卷调查 体检管理系统模块介绍 一、登记管理模块 登记体检者基本信息&#xff0c;包括唯一的体检编号&#xff0c;姓名、…

Windows 10+Visual Studio2019 Opencv-C++安装配置

前往官网下载需要的版本Releases - OpenCVhttps://opencv.org/releases/ 1.下载并解压OpenCV 我选择4.6.0&#xff0c;点击windows版本&#xff0c;进行下载 2.配置项目属性 打开你的Visual Studio 2019项目 -> 右击项目名&#xff0c;选择“属性” 注&#xff1a;整个配…

【C语言】指针基础知识理解【续】

1. ⼆级指针 指针变量也是变量&#xff0c;是变量就有地址&#xff0c;那指针变量的地址存放在哪⾥&#xff1f;这就是 ⼆级指针 。 1.1 引入二级指针 由于一级指针已经很熟悉&#xff0c;这里就不再赘述&#xff0c;这里我们重点探讨二级指针 下面先简单使用一个二级指针看…

机器学习之——支持向量机(SVM)技术详解

机器学习之——支持向量机&#xff08;SVM&#xff09;技术详解 1. 支持向量机的基本原理1.1 超平面1.1.1 定义与作用1.1.2 高维空间中的超平面 1.2 间隔最大化1.2.1 间隔的定义1.2.2 最大化间隔的原则 1.3 支持向量1.3.1 支持向量的定义1.3.2 支持向量的作用 2. SVM的数学基础…

Vue3 加载条(LoadingBar)

效果如下图&#xff1a;在线预览 APIs LoadingBar 参数说明类型默认值必传containerClass加载条容器的类名stringundefinedfalsecontainerStyle加载条容器的样式CSSProperties{}falseloadingBarSize加载条大小&#xff0c;单位 pxnumber2falsecolorLoading加载中颜色string‘…

快速识别音频文件转成文字

一、SenseVoice概述 阿里云通义千问开源了两款语音基座模型 SenseVoice&#xff08;用于语音识别&#xff09;和 CosyVoice&#xff08;用于语音生成&#xff09;。 SenseVoice 专注于高精度多语言语音识别、情感辨识和音频事件检测&#xff0c;有以下特点&#xff1a; 多语言…

4000元投影仪性价比之王:爱普生TW5750极米RS10还是当贝X5S?

买投影很多人会倾向于买大品牌或者是销量最好的那几款&#xff0c;首先是大品牌售后更有保障&#xff0c;口碑和销量也间接证明了这款投影是否值得买。这几年国内投影市场中爱普生、极米、当贝这三家投影品牌无论是在产品、口碑、售后服务等方面都是最好的&#xff0c;被用户们…

深入理解 Go 数组、切片、字符串

打个广告&#xff1a;欢迎关注我的微信公众号&#xff0c;在这里您将获取更全面、更新颖的文章&#xff01; 原文链接&#xff1a;深入理解 Go 数组、切片、字符串 欢迎点赞关注 前言 为什么在一篇文章里同时介绍数组、切片、字符串&#xff0c;了解这三个数据类型底层数据结构…