浅析Golang的Context

ops/2024/12/22 15:51:55/

文章目录

      • 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/ops/123072.html

相关文章

python 实现Greedy Best First Search最佳优先搜索算法

Greedy Best First Search最佳优先搜索算法介绍 Greedy Best First Search&#xff08;GBFS&#xff0c;贪婪最佳优先搜索&#xff09;是一种启发式搜索算法&#xff0c;用于在图或树形结构中寻找从起始节点到目标节点的最优路径。该算法的基本思想是在每一步选择当前看起来最…

Webpack 打包后文件过大,如何优化?

聚沙成塔每天进步一点点 本文回顾 ⭐ 专栏简介Webpack 打包后文件过大&#xff0c;如何优化&#xff1f;1. 代码分割&#xff08;Code Splitting&#xff09;1.1 概念1.2 Webpack 的 SplitChunksPlugin示例配置&#xff1a; 1.3 按需加载&#xff08;Lazy Loading&#xff09;示…

Docker 实践与应用举例

Docker 实践与应用举例 Docker 已经成为现代软件开发和部署中的重要工具&#xff0c;通过容器化技术&#xff0c;开发者可以轻松管理应用的依赖环境、简化部署流程&#xff0c;并实现跨平台兼容性。本篇博客将详细介绍 Docker 的基本概念、实践操作以及应用场景&#xff0c;帮…

Java面试题三

一、什么是Java中的封装、继承和多态&#xff1f; 在Java中&#xff0c;封装、继承和多态是面向对象编程&#xff08;OOP&#xff09;的三个核心概念&#xff0c;它们各自在软件设计和开发中扮演着重要的角色。 1. 封装&#xff08;Encapsulation&#xff09; 封装是隐藏对象…

C++学习笔记----8、掌握类与对象(四)---- 不同类型的数据成员(2)

3、引用数据成员 Spreadsheet与SpreadsheetCell是伟大的&#xff0c;但是不是它们自己就能成为有用的应用程序。需要代码去控制整个spreadsheet程序&#xff0c;可以将其打包成一个SpreadsheetApplication类。假定接下来我们要让每个Spreadsheet来保存一个应用程序对象的引用。…

HUAWEI_HCIA_实验指南_Lib1.4_配置通过Telnet登录系统

一、原理概述 Telnet(Telecommunication Network Protocol)起源于ARPANET,是最早的Internet应用之一。 Telnet 通常用在远程登录应用中&#xff0c;以便对本地或远端运行的网络设备进行配置、监控和维护。如网络中有多台设备需要配置和管理&#xff0c;用户无需为每一台设备…

DES数据加密标准

目录 一、算法原理每一轮的加密过程1. 初始分组2. 16轮迭代加密3. F函数&#xff08;核心加密步骤&#xff09;4. 16轮加密结束 密钥生成过程 二、代码实例 一、算法原理 DES&#xff08;数据加密标准&#xff09;算法对明文数据进行16轮的替换和置换操作&#xff0c;每一轮都…

Python字符串基本操作

目录 一、字符串的创建 1.1 转义字符 1.2 原始字符串 二、字符串的访问与切片 2.1 字符访问 2.2 切片(Slicing) 三、字符串的连接与重复 四、字符串的格式化 4.1 百分号格式化 4.2 str.format() 方法 4.3 f-字符串(Python 3.6及以上) 五、字符串的方法 5.1 大…