Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Go 并发控制】上下文 Context

【Go 并发控制】上下文 Context

作者头像
JuneBao
发布于 2022-10-26 07:10:55
发布于 2022-10-26 07:10:55
64900
代码可运行
举报
文章被收录于专栏:JuneBaoJuneBao
运行总次数:0
代码可运行

context ε(┬┬﹏┬┬)3

Context

在 Go 服务中,往往由一个独立的 goroutine 去处理一次请求,但在这个 goroutine 中,可能会开启别的 goroutine 去执行一些具体的事务,如数据库,RPC 等,同时,这一组 goroutine 可能还需要共同访问一些特殊的值,如用户 token, 请求过期时间等,当一个请求超时后,我们希望与此请求有关的所有 goroutine 都能快速退出,以回收系统资源。

context 包由谷歌开源,在 Go 1.7 时加入标准库,使用它可以很容易的把特定的值,取消信号, 截止日期传递给请求所涉及的所有 goroutine。

context 包的核心是 Context 接口,其结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Context interface {
    Done() <-chan struct{}
    Err() error
    Deadline() (deadline time.Time, ok bool)
    Value(key interface{}) interface{}
}
  1. Done 返回一个 chan, 表示一个取消信号,当这个通道被关闭时,函数应该立刻结束工作并返回。
  2. Err() 返回一个 error, 表示取消上下文的原因
  3. Deadline 会返回上下文取消的时间
  4. Value 用于从上下文中获取 key 对应的值

使用

传递取消信号(cancelation signals)

正如使用 chan 控制并发一样,我们希望传递给 goroutine 一个信号,一旦接收到这个信号,就立刻停止工作并返回,context 包提供了一个 WithCancel(), 使用它可以很方便的传递取消信号。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func useContext(ctx context.Context, id int) {
    for {
        select {
        case <- ctx.Done():
            fmt.Println("stop", id)
            return
        default:
            run(id)
        }
    }
}

func G2(ctx context.Context) {
    nCtx, nStop := context.WithCancel(ctx)
    go G4(nCtx)
    for {
        select {
        case <- ctx.Done():
            fmt.Println("stop 2")
            nStop()
            return
        default:
            run(2)
        }
    }
}

func G3(ctx context.Context) {
   useContext(ctx, 3)
}

func G4(ctx context.Context) {
    useContext(ctx, 4)
}

func main() {
    ctx, done := context.WithCancel(context.Background())
    go G2(ctx)
    go G3(ctx)
    time.Sleep(5*time.Second)
    done()
    time.Sleep(5*time.Second)
}

设置截止时间

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func G6(ctx context.Context) {
    for  {
        select {
        case <- ctx.Done():
            t, _ := ctx.Deadline()
            fmt.Printf("[*] %v done: %v\n", t, ctx.Err())
            return
        default:
            fmt.Println("[#] run ...")
        }
    }
}

func main() {
    // ctx, done := context.WithTimeout(context.Background(), time.Second * 2)
    ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)
    go G6(ctx)
    //done()
    time.Sleep(10*time.Second)
}

[#] run ...
...
[*] 2020-10-31 20:24:42.0581352 +0800 CST m=+2.008975001 done: context deadline exceeded

传值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func G7(ctx context.Context) {
    for  {
        select {
        case <- ctx.Done():
            fmt.Println("cancel", ctx.Value("key"))
            return
        default:
            fmt.Println("running ", ctx.Value("key"))
            time.Sleep(time.Second)
       }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)
    ctx =  context2.WithValue(ctx, "key", "value")
    go G7(ctx)
    time.Sleep(10*time.Second)
}

context 包概览

context 包的核心是 context.Context 接口,另外有四个 struct 实现了 Context 接口,分别是 emptyCtx, cancelCtx, timerCtx, valueCtx, 其中 emptyCtx 是一个默认的空结构体,其余三个都是在其基础上添加了各自功能的实现,针对 emptyCtx ,context 包中暴露了两个方法 Background()TODO() 去创建一个空的 emptyCtx, 而针对后面三种具体的 struct ,context 包总共暴露了四个方法去产生对应的 struct, 他们分别是: WithCancel(), WithDeadLine(), WithTimeout(), WithValue(),对应关系如下:

TODO 和 Background

TODO 和 Background 方法用来返回一个 emptyCtx 类型,他们在实现上都一样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

这两个方法都会返回一个非空的上下文 emptyCtx,他永远不会被取消,用于传递给其他方法去构建更加复杂的上下文对象,一般默认使用 Background(), 只有在不确定时使用TODO(), 但实际上他们只是名字不同而已。

下面是 emptyCtx 的实现,他确实没做任何事。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 interface{}) interface{} {
   return nil
}

WithCancel

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type cancelCtx struct {
    Context

    mu       sync.Mutex            // 用于同步
    done     chan struct{}         // 会在 Done 中返回
    children map[canceler]struct{} // 子上下文列表,done 被关闭后,会遍历这个 map,关闭所有的子上下文
    err      error                 // 关闭 chan 产生的异常,在初始化时会被赋值使不为空
}

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
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

当调用 WithCancel 时, 首先会根据 parent 拷贝一个新的 cancelCtx:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

然后会调用 propagateCancel 安排子上下文在父上下文结束时结束,最后除了 cancelCtx 的引用外还会返回一个 func, 该方法里调用了 c.cancel(), 也就是当我们调用 done() 时,调用的其实是 c.cancel()

cancel

cancel 的作用是关闭 当前上下文以及子上下文的cancelCtx.done 管道。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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     // 已经关闭,返回
    }
    c.err = err    // 通过 err 标识已经关闭
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)   // 关闭当前 done
    }
    // 由于是 map, 所以关闭顺序是随机的
    for child := range c.children {
        child.cancel(false, err)   // 遍历取消所有子上下文
    }
    c.children = nil    // 删除子上下文
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)   // 从父上下文删除自己
    }
}

propagateCancel

该函数的作用是保证父上下文结束时子上下文也结束,一方面,在生成子上下文的过程中,如果父亲已经被取消,那 child 也会被关闭,另一方面,如果在执行过程中父上下文一直开启,那就正常把子上下文加入到父上下文的 children 列表中等执行 cancel再关闭。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    // 如果父亲的 Done 方法返回空,说明父上下文永远不会被取消
    // 这种情况对应 ctx, done := context.WithCancel(context.Background())
    if done == nil {
        return 
    }
    
    // 如果到这父上下文已经被取消了,就关闭当前上下文
    select {
    case <-done:
        child.cancel(false, parent.Err())
        return
    default:
    }
    
    // 父亲没有被取消
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        // 父亲已经取消,关闭自己
        if p.err != nil {
            child.cancel(false, p.err)
        } else {
            // 把 child 加到 parent 的 children 中
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        // 父上下文是开发者自定义的类型, 开启一个 goroutine 监听父子上下文直到其中一个关闭
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

WithTimeout 和 WithDeadline

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}

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

timerCtx是在 cancelCtx的基础上添加了一个定时器和截止时间实现的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    // 如果传入的截止时间比父上下文的截止时间晚,也就是说父上下文一定会比子上下文先结束
    // 这种情况下给子上下文设置截止时间是没有任何意义的,所以会直接创建一个 cancelCtx
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        return WithCancel(parent)
    }
    // 构建新的 timerCtx
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    // 保证子上下文在父上下文关闭时关闭
    propagateCancel(parent, c)
    // 计算当前距离截止时间 d 还有多长时间
    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()
    // c.err == nil 说明当前上下文还没有被关闭
    if c.err == nil {
        // AfterFunc 等待 dur 后会开启一个 goroutine 执行 传入的方法,即 c.cancel
        // 并会返回一个计时器 timer,通过调用 timer 的 Stop 方法可以停止计时取消调用。
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

timerCtxcancel 方法主要还是调用了 cancelCtx.cancel

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    // 调用 cancelCtx.cancel,关闭子上下文
    c.cancelCtx.cancel(false, err)
    // 从父上下文中删除当前上下文
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        // 停止计时,取消调用
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

WithTimeout 直接调用了 WithDeadline

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

WithValue

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func WithValue(parent Context, key, val interface{}) Context {
    // key 不能为 nil
    if key == nil {
        panic("nil key")
    }
    // key 必须是可比较的
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

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

The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. key 请尽量使用自定义的 struct{}, 避免使用内置数据类型以避免使用 context 包时的冲突

总结

context 包是 Go 1.7 后加入的一种用于复杂场景下并发控制的模型,最核心的接口是 context.Context, 这个结构体中定义了五个待实现的方法,用来实现发送关闭信号,设置 dateline,传递值等功能。

context 包的核心思想是以 树形 组织 goroutine, 创建新上下文时需要给他指定一个父上下文,由此,根上下文对应根 goroutine, 子上下文对应子 Goroutine, 实现灵活的并发控制。

rootContext 一般通过 Background()TODO() 创建,他们会创建一个空的 emptyCtx, 然后如果想要使用 context 包的具体功能,可以使用 WithCancel()WithDateline()WithValue() 将父上下文包装成具体的上下文对象(cancelCtx, timerCtx, valueCtx),前两个方法会返回两个值 (ctx Context, done func()) 调用 done 可以向 goroutine 发送一个关闭信号, goroutine 中监控 ctx.Done() 便可得到这个信号。

cancelCtxtimerCtx 会保持一个 childrentimerCtx 实际上是继承了 cancelCtx),这是一个 map key 是 canceler , Value 是 struct{} 类型,值并没什么用,在创建 cancelCtxtimerCtx时,会把当前上下文加入到其父亲的 children 中,在父上下文关闭时会遍历 children 关闭所有的子上下文,并将本上下文从其父上下文的 children 中删除,由于 map 遍历的无序性,子上下文关闭的顺序也是随机的。

WithValue() 以及 valueCtx 的实现稍微与前两个有所不同,一方面 valueCtx 没有自己实现 Done(), Deadline() 等方法,所以其功能仅限于传值,另外,在 WithValue() 中并没有调用 propagateCancel(), 所以 valueCtx 并不会被放在父上下文的 children 中,他自己也没有 children, 所以使用 valueCtx 作为父上下文是没有意义的。

如非必要,一般无需使用 WithValue() 的功能传值,他一般用在传递请求对应用户的认证令牌或用于进行分布式追踪的请求 ID中。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-11-1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
go context详解
在 Go 服务器中,每个传入的请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问数据库和 RPC 服务等后端。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,所有处理该请求的 goroutines 都应该快速退出,以便系统可以回收它们正在使用的任何资源。
Johns
2022/06/22
2K0
深入Go:Context
在理解了 package context 的使用后,我们很自然地想问其背后的设计哲学有什么?实际上,我们发现无论是在关于 Context 的批评/讨论也不少,那么 Context 的设计合不合理?带着这些疑虑,我们深入 context 的源码,尝试对这些问题作出解答。
wenxing
2021/12/14
8470
深入Go:Context
轻松上手!手把手带你掌握从Context到go设计理念
: 导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后端开发工程师陈雪锋。 context包比较小,是阅读源码比较理想的一个入手,并且里面也涵盖了许多go设计理念可以学习。 go的Context作为go并发方式的一种,无论是在源码net/http中,开源框架例如gin中,还是内部框架trpc-go中都是一个比较重要的存在,而整个 c
腾讯云开发者
2022/09/27
4200
轻松上手!手把手带你掌握从Context到go设计理念
Go语言上下文Context包源码分析和实践
context包最早在golang.org/x/net/context中,在Go1.7时,正式被官方收入,进入标准库,目前路径为src/context/,目前context包已经在Go各个项目中被广泛使用。并且在Co中Context和并发编程有着密切的关系(context ,chan ,select,go这些个词经常密不可分)
阿伟
2019/12/17
9110
go context原理
在 go 语言开发中, context 用于提供上下文的联系, 在不同协程调用间建立取消和超时机制,也可以用于传递相关值。
leobhao
2024/04/01
1680
一日一学_Go语言Context(设计及分析)
Go服务器的每个请求都有自己的goroutine,而有的请求为了提高性能,会经常启动额外的goroutine处理请求,当该请求被取消或超时,该请求上的所有goroutines应该退出,防止资源泄露。那
李海彬
2018/03/28
1.1K0
一日一学_Go语言Context(设计及分析)
浅析golang中的context
go1.7才引入context,译作“上下文”,实际也叫goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息、context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine
素履coder
2022/02/17
1.1K0
浅析golang中的context
Go Context解析 A Brief Inquiry Into Go Context
Package context defines the Context type, which carries deadlines,
takeonme.
2021/11/26
9593
[Golang]Context详解
Context 是 Golang 中非常有趣的设计,它与 Go 语言中的并发编程有着比较密切的关系,在其他语言中我们很难见到类似 Context 的东西,它不仅能够用来设置截止日期、同步『信号』还能用来传递请求相关的值。
宇宙无敌暴龙战士之心悦大王
2023/04/07
8880
Context源码,再度重相逢
各位读者朋友们大家好,我是随波逐流的薯条。深秋了,前几天气温骤降,北京的人和狗都不愿意出门,趴在窝里冻的打寒颤。我的书房里没装空调,暖气要十一月中旬才来,每次想学习都得下很大的决心,所以这篇文章发出来时比预期又晚了几天~
薯条的编程修养
2022/08/10
2660
Context源码,再度重相逢
Golang context 包入门
概述 Golang 的 context Package 提供了一种简洁又强大方式来管理 goroutine 的生命周期,同时提供了一种 Requst-Scope K-V Store。但是对于新手来说,Context 的概念不算非常的直观,这篇文章来带领大家了解一下 Context 包的基本作用和使用方法。 1. 包的引入 在 go1.7 及以上版本 context 包被正式列入官方库中,所以我们只需要import "context"就可以了,而在 go1.6 及以下版本,我们要 import "golang
李海彬
2018/03/26
1.1K0
Go组件:context学习笔记!
导语 | 最近学习go有一段时间了,在网上一直看到别人推荐,学go可以学习里面的context源码,短小精悍。看了下确实有所收获,本文是基于我最近对context源码学习的一些心得积累,望大家不吝赐教。 一、为什么使用Context (一)go的扛把子 要论go最津津乐道的功能莫过于go强大而简洁的并发能力。 func main(){ go func(){ fmt.Println("Hello World") }()} 通过简单的go func(){},go可以快速生成新的协程并运行。
腾讯云开发者
2022/08/26
4110
Go组件:context学习笔记!
深入理解Golang之Context
这篇文章将介绍Golang并发编程中常用到一种编程模式:context。本文将从为什么需要context出发,深入了解context的实现原理,以及了解如何使用context。
KevinYan
2020/03/12
8910
深入解析Golang之context
context翻译成中文就是上下文,在软件开发环境中,是指接口之间或函数调用之间,除了传递业务参数之外的额外信息,像在微服务环境中,传递追踪信息traceID, 请求接收和返回时间,以及登录操作用户的身份等等。本文说的context是指golang标准库中的context包。Go标准库中的context包,提供了goroutine之间的传递信息的机制,信号同步,除此之外还有超时(timeout)和取消(cancel)机制。概括起来,Context可以控制子goroutine的运行,超时控制的方法调用,可以取消的方法调用。
数据小冰
2022/08/15
1.4K0
深入解析Golang之context
Go context.WithCancel()的使用
WithCancel可以将一个Context包装为cancelCtx,并提供一个取消函数,调用这个取消函数,可以Cancel对应的Context Go语言context包-cancelCtx[1]
fliter
2023/09/05
2240
Go context.WithCancel()的使用
Go 并发模式: context.Context 上下文详解
Package context 中定义了 Context 类型, 用于跨 API 或跨进程之间传递数据,包含 deadlines, cancellation signals, 以及其他 request-scoped values 。
一个会写诗的程序员
2022/05/13
1.4K1
Go 并发模式: context.Context 上下文详解
Go Context 详解之终极无惑
Go 1.7 标准库引入 Context,中文名为上下文,是一个跨 API 和进程用来传递截止日期、取消信号和请求相关值的接口。
恋喵大鲤鱼
2022/05/09
5.3K0
Go Context 详解之终极无惑
Golang——Context
Go中goroutine之间没有父与子的关系,多个gorountine都是平行的被调度,不存在所谓的子进程退出后的通知机制。多个goroutine协调工作涉及 通信,同步,通知,退出 四个方面: 通信:chan通道是各goroutine之间通信的基础。注意这里的通信主要指程序的数据通道。 同步:可以使用不带缓冲的chan;sync.WaitGroup为多个gorouting提供同步等待机制;mutex锁与读写锁机制。 通知:通知与上文通信的区别是,通知的作用为管理,控制流数据。一般的解决方法是在输入端绑定两个chan,通过select收敛处理。这个方案可以解决简单的问题,但不是一个通用的解决方案。 退出:简单的解决方案与通知类似,即增加一个单独的通道,借助chan和select的广播机制(close chan to broadcast)实现退出。 context设计目的: 1.退出通知机制一一通知可以传递给整个 goroutine 调用树上的每一个。 2.传递数据一一数据可 以传递给整个 goroutine 调用树上的每一个 goroutine
羊羽shine
2019/05/29
1K1
Go进阶(3):上下文context
在 Go http包的Server中,每一个请求在都有一个对应的 goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和RPC服务。一个上游服务通常需要访问多个下游服务,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
黄规速
2023/02/27
7910
Go进阶(3):上下文context
Golang 并发 与 context标准库
这篇文章将:介绍context工作机制;简单说明接口和结构体功能;通过简单Demo介绍外部API创建并使用context标准库;从源码角度分析context工作流程(不包括mutex的使用分析以及timerCtx计时源码)。
李海彬
2019/05/08
8090
Golang 并发 与 context标准库
相关推荐
go context详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档