浅析Golang的Context

news/2024/10/4 11:02:18/

文章目录

      • 1. 简介
      • 2. 常见用法
        • 2.1 控制goroutine的生命周期(cancel)
        • 2.2 传递超时(Timeout)信息
        • 2.3 传递截止时间(Deadline)
        • 2.4 传递请求范围内的全局数据 (value)
      • 3 特点
        • 3.1 上下文的不可变性
        • 3.2 链式传递
        • 3.3 超时和截止时间
        • 3.4 多级取消机制
        • 3.5 context.Value() 传递全局数据
        • 3.6 context 线程安全
        • 3.7 层次化的取消信号传播
      • 4 底层实现原理
        • 4.1 Context接口
        • 4.2 emptyCtx类
        • 4.3 cancelCtx类
        • 4.4 timerCtx类
        • 4.5 valueCtx类

1. 简介

在 Go 语言中,context 包主要用于在 并发编程 中控制和管理 goroutine 的生命周期。它提供了一种机制,可以通过传递 context.Context 来协调多个 goroutine,特别是在需要取消操作超时控制传递共享数据时。

2. 常见用法

2.1 控制goroutine的生命周期(cancel)

context 允许父 goroutine 可以通知子 goroutine 停止工作。例如,当你在 HTTP 服务器中处理一个请求时,如果客户端关闭了连接,你可能不再需要继续处理这个请求。通过 context,你可以在主流程中取消子流程(即 goroutine),避免不必要的资源消耗。

ctx, cancel := context.WithCancel(context.Background())
go func() {select {case <-ctx.Done():// 接收到取消信号,停止工作fmt.Println("Goroutine canceled")}
}()
// 取消操作
cancel()
2.2 传递超时(Timeout)信息

context 还可以传递一个超时时间,允许操作在指定时间后自动停止。这对于需要限定时间完成的操作(如数据库查询、API 调用)非常有用。

ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()select {
case <-time.After(time.Second * 1):fmt.Println("Operation completed within timeout")
case <-ctx.Done():fmt.Println("Operation timed out")
}
2.3 传递截止时间(Deadline)

context 可以包含一个截止时间(Deadline),这是在某个具体的时间点之后,所有的操作都应该停止。与超时类似,但它使用绝对时间而不是相对时间 。

deadline := time.Now().Add(time.Second * 5)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()select {
case <-time.After(time.Second * 6):fmt.Println("Operation completed")
case <-ctx.Done():fmt.Println("Deadline exceeded")
}
2.4 传递请求范围内的全局数据 (value)

context 还可以携带一些全局数据,虽然 context 设计的初衷并不是为了数据传递,但在一些场景下,利用 context 传递与请求上下文相关的信息是很常见的做法。例如,在 Web 服务中传递用户认证信息、跟踪请求 ID 等。

package mainimport ("context""fmt"
)// 创建一个 key 类型,用于在 context 中存储和检索数据
type key stringconst userIDKey key = "userID"// 传递用户 ID 的函数
func processRequest(ctx context.Context) {// 从 context 中取出用户 IDuserID := ctx.Value(userIDKey)if userID != nil {fmt.Println("User ID found in context:", userID)} else {fmt.Println("No User ID found in context")}
}func main() {// 创建一个带有用户 ID 的 contextctx := context.WithValue(context.Background(), userIDKey, "12345")// 传递带有用户 ID 的 contextprocessRequest(ctx)// 调用时没有用户 IDprocessRequest(context.Background())
}

3 特点

3.1 上下文的不可变性

context 是不可变的。一旦创建了一个 context,你不能修改它。每次你想要添加新的值、超时或取消机制时,都会创建一个新的子 context。这有助于保持 context 的并发安全,确保多个 goroutine 可以共享同一个 context 而不发生数据竞争。

例如,使用 context.WithValue 添加键值对时,实际上是创建了一个新的 context,旧的 context 保持不变。

parentCtx := context.Background()
childCtx := context.WithValue(parentCtx, "key", "value")
3.2 链式传递

context 是链式传递的。你可以从一个父 context 创建多个子 context,并且这些子 context 可以继续创建它们的子 context。这种设计允许你在复杂的程序中,管理多个 context,并且可以确保每个子 context 都能够追溯到其父 context

例如,当你调用 context.WithCancelcontext.WithTimeout 时,都会基于父 context 创建新的子 context

3.3 超时和截止时间

context 允许你为操作设置 超时时间截止时间。通过 context.WithTimeoutcontext.WithDeadline,你可以确保某些操作在指定时间内完成,否则自动取消。这对于网络请求、数据库查询等可能需要限制执行时间的操作非常有用。

  • context.WithTimeout:为上下文设置相对的超时时间。例如,在 5 秒后自动取消。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
  • context.WithDeadline:为上下文设置绝对的截止时间。例如,在某个具体的时间点后取消。
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

在这两种情况下,一旦时间到了,ctx.Done() 通道会关闭,所有使用这个 context 的操作都会被取消。

3.4 多级取消机制

除了父 context 取消时会取消所有子 context 外,子 context 也可以通过独立的取消机制取消自己。例如,如果你为某个子 context 设置了独立的超时或调用了它的 cancel 函数,子 context 会取消,而其他的兄弟 context 或父 context 不受影响。

parentCtx, parentCancel := context.WithCancel(context.Background())
childCtx, childCancel := context.WithTimeout(parentCtx, 2*time.Second)go func() {<-childCtx.Done()fmt.Println("Child context cancelled")
}()time.Sleep(3 * time.Second)
parentCancel() // 取消父 context

在这个例子中,childCtx 会因为超时先被取消,而 parentCtx 只有在调用 parentCancel() 后才会取消。

3.5 context.Value() 传递全局数据

虽然 context 的设计主要是用于控制 goroutine 的生命周期,但它也可以通过 context.WithValue 传递少量的全局数据(键值对)。常见的使用场景包括在请求链路中传递用户认证信息、请求 ID 等。

注意:

  • context.Value() 传递的数据应该是与请求相关的少量数据,而不应当被滥用为数据存储机制。
  • 使用自定义的键类型(如 type key string)可以避免键冲突。
type key string
ctx := context.WithValue(context.Background(), key("userID"), "12345")
userID := ctx.Value(key("userID"))
fmt.Println("User ID:", userID)
3.6 context 线程安全

context 是设计为并发安全的。你可以将同一个 context 实例传递给多个 goroutine,而不需要担心数据竞争或同步问题。因为 context 的状态(如取消、超时)通过不可变的方式和通道传播,它天然支持并发。

func worker(ctx context.Context, id int) {select {case <-time.After(3 * time.Second):fmt.Printf("Worker %d done\n", id)case <-ctx.Done():fmt.Printf("Worker %d cancelled\n", id)}
}func main() {ctx, cancel := context.WithCancel(context.Background())for i := 1; i <= 3; i++ {go worker(ctx, i)}time.Sleep(1 * time.Second)cancel() // 取消所有子 goroutinetime.Sleep(4 * time.Second)
}
3.7 层次化的取消信号传播

context 结构中,当父 context 被取消时,所有子 context 会自动取消,这是一种层次化的取消信号传播机制。这可以帮助你在复杂系统中控制多个 goroutine 的生命周期。例如,在 Web 服务中,当用户取消请求时,所有与该请求相关的操作都应该立即停止。

context 被取消时,context.Err() 会返回具体的错误类型:

  • context.Canceled:表示上下文被显式取消(例如,调用了 cancel())。
  • context.DeadlineExceeded:表示上下文超时或截止时间已过。

4 底层实现原理

主要通过解析context的源码来剖析第三节中各个特点的实现机理。

4.1 Context接口
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}

这是context最核心的一个接口,定义了Context接口,包含了四个方法,其他结构体只要实现了这四个方法就实现了该接口。四个方法的功能分别是:

  • Deadline() (deadline time.Time, ok bool):Deadline 方法返回一个时间(time.Time),表示在这个上下文下执行的任务应该取消的截止时间。通常用在需要在某个时间点前完成的任务中。如果 context 没有设置任何截止时间,Deadline() 的返回值中,ok 会是 false。这意味着当前 context 没有时间限制,因此任务可以无限期地运行,除非被手动取消。对 Deadline() 的连续调用会返回相同的结果。
  • Done() <-chan struct{}:Done() 方法返回一个通道,用于通知 goroutine 某个 context 关联的任务应该被取消。Done() 返回一个通道(chan struct{}),这个通道在 context 关联的工作需要被取消时会关闭。你可以通过监听这个通道来决定是否继续执行某个操作。
  • Err() error:Err() 方法用于返回 context 的取消原因,尤其在 Done() 通道关闭之后,它提供了上下文取消的具体原因。如果 Done() 通道还没有关闭(即 context 没有被取消或超时),调用 Err() 方法将返回 nil。如果 Done() 通道已经关闭,Err() 方法会返回一个非空的错误(error),这个错误解释了为什么 context 被取消。(1) 如果 context 是由于调用 cancel() 函数而被取消的,Err() 将返回一个 context.Canceled 错误。这表明操作是手动取消的。 (2) 如果 context 是因为超时或截止时间到达而被取消的,Err() 将返回一个 context.DeadlineExceeded 错误。这表明操作是因为超时被取消的。
  • Value(key any) any:Value() 方法根据传入的键(key),返回与该键相关联的值。如果该键没有与任何值相关联,则返回 nil。 键的类型可以是支持相等性判断的任何类型(通常是简单的标识符类型)。为了避免不同包之间的键冲突,应该将键定义为未导出类型(即,首字母小写的类型)。
4.2 emptyCtx类

emptyCtx 是一种特殊的上下文,它永远不会被取消、没有任何关联的值,也没有截止时间。它是 context.Background()context.TODO() 的基础类型。

type emptyCtx struct{}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
}

context.Background()context.TODO()

func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}type backgroundCtx struct{ emptyCtx }func (backgroundCtx) String() string {return "context.Background"
}type todoCtx struct{ emptyCtx }func (todoCtx) String() string {return "context.TODO"
}

context.Background():这是 Go 程序中通常用作根上下文的上下文。它一般用于顶层的 context,例如在启动 goroutine、处理请求、初始化程序时使用。由于它基于 emptyCtx,所以它不会被取消、没有值或截止时间。

ctx := context.Background()

context.TODO()TODO() 通常用于代码还在编写过程中,开发者不确定应该使用哪种 context 的场景下。它是一个临时的占位符,表示将来会替换为合适的上下文。和 Background() 一样,TODO() 也是基于 emptyCtx,没有取消、值或截止时间。

ctx := context.TODO()
4.3 cancelCtx类
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel callcause    error                 // set to non-nil by the first cancel call
}// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}

cancelCtx 是可以被取消的,当 cancelCtx 被取消时,它会自动取消所有实现了 canceler 接口的子上下文。子上下文需要实现 canceler 接口,canceler 接口通常会有一个 cancel() 方法,用于执行取消操作。一旦父上下文的 cancel() 被调用,父上下文会遍历所有子上下文,调用它们的 cancel() 方法,将取消信号逐层向下传递。

func (c *cancelCtx) Value(key any) any {if key == &cancelCtxKey {return c}return value(c.Context, key)
}func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}
  • func (c *cancelCtx) Value(key any) any:如果 key 与特定的 cancelCtxKey 相等,则返回当前 cancelCtx 实例本身;否则,调用嵌套的父上下文的 Value 方法查找值。
  • func (c *cancelCtx) Done() <-chan struct{}:第一次调用 Done():如果 done 通道还没有创建,程序会在加锁的情况下懒加载创建这个通道。done 通道被存储在 c.done 中,确保后续的 Done() 调用直接返回这个通道。后续调用 Done():后续调用 Done() 时,会直接通过 c.done.Load() 读取已经创建的 done 通道,避免不必要的锁操作,提高了性能。
  • func (c *cancelCtx) Err() error:在锁保护下获取err
func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}c := &cancelCtx{}c.propagateCancel(parent, c)return c
}func (c *cancelCtx) propagateCancel(parent Context, child canceler) {c.Context = parentdone := parent.Done()if done == nil {return // parent is never canceled}select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}if a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()stop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})c.Context = stopCtx{Context: parent,stop:    stop,}c.mu.Unlock()return}goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}// parentCancelCtx(parent) 是一个辅助函数,检查父上下文是否是 cancelCtx 类型,
// 或者是否派生自 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
}

propagateCancel实现了 cancelCtx 中的 propagateCancel 方法,它的作用是将取消信号从父上下文(parent)传播到子上下文(child),并根据不同的情况处理取消逻辑。该方法负责建立父子上下文之间的取消关系,当父上下文被取消时,子上下文也会自动取消。这是 Go 语言 context 机制中取消信号传播的核心部分。

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 = errc.cause = caused, _ := 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 = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}

cancel() 方法用于取消当前上下文,并传播取消信号给它的子上下文。

func removeChild(parent Context, child canceler) {if s, ok := parent.(stopCtx); ok {s.stop()return}p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}

removeChild将子上下文从父上下文中移除。

4.4 timerCtx类
type timerCtx struct {cancelCtxtimer *time.Timer deadline time.Time
}

timerCtxcontext 的一种实现,它用于处理超时(timeout)或截止时间(deadline)。通过嵌入 cancelCtxtimerCtx 实现了取消上下文的能力,并通过计时器控制上下文的超时行为。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}func WithDeadlineCause(parent Context, d time.Time, cause error) (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{deadline: d,}c.cancelCtx.propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded, cause) // deadline has already passedreturn c, func() { c.cancel(false, Canceled, nil) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded, cause)})}return c, func() { c.cancel(true, Canceled, nil) }
}
  • WithTimeouWithDeadline:用于创建带有超时或截止时间的上下文。超时后自动取消上下文,返回 context.DeadlineExceeded 错误。
  • WithDeadlineCause:核心逻辑处理,创建上下文并设置定时器,定时器到期时自动取消上下文。它还支持设置取消原因(cause)。
  • 取消函数 CancelFunc:无论上下文是否超时,调用 CancelFunc 都可以立即取消上下文。
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 = errc.cause = caused, _ := 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 = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}

它会关闭与取消相关的通道,取消所有子上下文,必要时将当前上下文从父上下文中移除,并且在第一次取消时设置取消原因。

4.5 valueCtx类
type valueCtx struct {Contextkey, val any
}
  • valueCtx 同样继承了一个 parent context;
  • 一个 valueCtx 中仅有一组 kv 对.
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}
}
  • 倘若 parent context 为空,panic;
  • 倘若 key 为空 panic;
  • 倘若 key 的类型不可比较,panic;
  • 包括 parent context 以及 kv对,返回一个新的 valueCtx.
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.Contextcase *cancelCtx:if key == &cancelCtxKey {return c}c = ctx.Contextcase withoutCancelCtx:if key == &cancelCtxKey {// This implements Cause(ctx) == nil// when ctx is created using WithoutCancel.return nil}c = ctx.ccase *timerCtx:if key == &cancelCtxKey {return &ctx.cancelCtx}c = ctx.Contextcase backgroundCtx, todoCtx:return nildefault:return c.Value(key)}}
}
  • 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
  • 假如不等,则从 parent context 中依次向上寻找.

http://www.ppmy.cn/news/1534376.html

相关文章

边缘自适应粒子滤波(Edge-Adaptive Particle Filter)的MATLAB函数示例,以及相应的讲解

目录 讲解 初始化 预测步骤 观测模拟 权重更新 重采样 状态估计 总结 下面是一个简单的边缘自适应粒子滤波&#xff08;&#xff09;的函数示例&#xff0c;以及相应的讲解。 程序源代码&#xff1a; function X_est edgeAdaptiveParticleFilter(numParticles, numS…

Linux系统字符命令关机方法对比

一、相同点&#xff1a;都可以达到关机或重启系统的目的。 二、不同点&#xff1a;命令内部的工作过程不同。 1、shutdown 安全的关机命令&#xff1a;系统管理员会通知所有登录的用户系统将要关闭且 login 指令会被冻结&#xff0c;即新的用户不能再登录。根据使用的参数不同…

会声会影导出视频mp4格式哪个最高清,会声会影输出格式哪个清晰

调高分辨率后&#xff0c;mp4视频还是不清晰。哪怕全部使用4K级素材&#xff0c;仍然剪不出理想中的高画质作品。不是你的操作有问题&#xff0c;而是剪辑软件没选对。Corel公司拥有全球顶尖的图像处理技术&#xff0c;该公司研发的会声会影视频剪辑软件&#xff0c;在过去的20…

vue2 + View design 使用inputNumber设置默认值为undefined但展示数据为1且表单校验不通过的原因

文章目录 一、背景二、操作步骤1.复现前的准备工作&#xff08;1&#xff09;vue版本和view design 版本&#xff08;2&#xff09;创建一个组件&#xff08;组件中根据类型渲染不同的组件&#xff09;&#xff08;3&#xff09;在list.vue页面中引入组件&#xff0c;传入配置&…

ufw:Linux网络防火墙

一、命令简介 ​ufw​&#xff08;Uncomplicated Firewall&#xff09;是一个为 Linux 系统提供简单易用的命令行界面的防火墙管理工具。它是基于 iptables ​的&#xff0c;但提供了更简洁的语法和更直观的操作方式&#xff0c;使得配置防火墙变得更加简单&#xff0c;特别适…

HTML5实现唐朝服饰网站模板源码

文章目录 1.设计来源1.1 网站首页-界面效果1.2 唐装演变-界面效果1.3 唐装配色-界面效果1.4 唐装花纹-界面效果1.5 唐装文化-界面效果 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcL…

SpringBoot集成-RocketMQ快速入门

1.MQ概述 MQ全称为Message Queue&#xff0c;即消息队列 &#xff0c;是一种提供消息队列服务的中间件&#xff0c;也称为消息中间件&#xff0c;是一套提供了消息生 产、存储、消费全过程的软件系统&#xff0c;遵循FIFO原则。 1.1MQ常见产品 ActiveMQ ActiveMQ是使用Java语…

水波荡漾效果+渲染顺序+简单UI绘制

创建场景及布置 创建新场景Main,在Main场景中创建一个plane物体&#xff0c;命名为WaterWavePla,具体数值及层级面板排布如下&#xff1a; 编写脚本 创建一个文件夹&#xff0c;用于存放脚本&#xff0c;命名Scripts,创建一个子文件夹Effect,存放特效相关脚本&#xff0c;创建…