go语言context(设计及分析)

张伯雨 张伯雨     2022-09-24     348

关键词:

context简单概述:

Go服务器的每个请求都有自己的goroutine,而有的请求为了提高性能,会经常启动额外的goroutine处理请求,当该请求被取消或超时,该请求上的所有goroutines应该退出,防止资源泄露。那么context来了,它对该请求上的所有goroutines进行约束,然后进行取消信号,超时等操作。

而context优点就是简洁的管理goroutines的生命周期。

context简单使用:

接下来模拟一个超时继续分析context

注意: 使用时遵循context规则
1. 不要将 Context放入结构体,Context应该作为第一个参数传
   入,命名为ctx。
2. 即使函数允许,也不要传入nil的 Context。如果不知道用哪种 
   Context,可以使用context.TODO()。
3. 使用context的Value相关方法,只应该用于在程序和接口中传递
   和请求相关数据,不能用它来传递一些可选的参数
4. 相同的 Context 可以传递给在不同的goroutine;Context 是
   并发安全的。

使用net/http/pprof对goroutines进行查看:

package main

import (
    "context"
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "time"
)

func main() {
    go http.ListenAndServe(":8080", nil)
    ctx, _ := context.WithTimeout(context.Background(), (10 * time.Second))
    go testA(ctx)
    select {}
}

func testA(ctx context.Context) {
    ctxA, _ := context.WithTimeout(ctx, (5 * time.Second))
    ch := make(chan int)
    go testB(ctxA, ch)

    select {
    case <-ctx.Done():
        fmt.Println("testA Done")
        return
    case i := <-ch:
        fmt.Println(i)
    }

}

func testB(ctx context.Context, ch chan int) {
    //模拟读取数据
    sumCh := make(chan int)
    go func(sumCh chan int) {
        sum := 10
        time.Sleep(10 * time.Second)
        sumCh <- sum
    }(sumCh)

    select {
    case <-ctx.Done():
        fmt.Println("testB Done")
        <-sumCh
        return
    //case ch  <- <-sumCh: 注意这样会导致资源泄露
    case i := <-sumCh:
        fmt.Println("send", i)
        ch <- i
    }

}

模拟数据库读取效率慢

从执行中和执行后的结果来看,我们完美的关闭了不需要的goroutine,


超时

概述中提到:
当应用场景是由一个请求衍生出多个goroutine完成需求,那它们之间就需要满足一定的约束关系,才能中止routine树,超时等操作。
那么是如何到达约束关系?
简单理解,Context 的调用以链式存在,通过WithXxx方法派生出新的 Context与当前父Context 关联,当父 Context 被取消时,其派生的所有 Context 都将取消。


WithCancel派生约束关系图

上图WithCancel派生简单分析图,接下里我们进行源码分析,进一步了解Context的约束关系(WithXxx方法派生大概与WithCancel相同)。

Context源码分析:

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

Deadline() 返回的time.Time 它为Context 的结束的时间,ok 表示是否有 deadline
Done() 返回一个信道,当对Context进行撤销或过期时,该信道就会关闭的,可以简单认为它是关闭信号。
Err() 当Done信道关闭后,Err会返回关闭的原因(如超时,手动关闭)

Value(key interface{}) 一个 K-V 存储的方法

canceler提供了cancal函数,同时要求数据结构实现Context

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
//默认错误
var Canceled = errors.New("context canceled")
var DeadlineExceeded = errors.New("context deadline exceeded")

实现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 interface{}) interface{} {
    return nil
}

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

两个实现Context的空结构。

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


func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

cancelCtx结构体继承了Context,同时实现了canceler接口:

type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // 当被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    //timerCtx会频繁使用这块代码,因为派生出来    
    //timerCtx全部指向同一个cancelCtx.
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    //关闭c.done
    close(c.done)
    //依次遍历c.children,每个child分别cancel
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()
    //如果removeFromParent为true,则将c从其parent的children中删除
    if removeFromParent {
        removeChild(c.Context, c) 
    }
}
//如果parent为valueCtx类型,将循环找最近parent
//为CancelCtx类型的,找到就从父对象的children 
//map 中删除这个child,否则返回nil(context.Background或者 context.TODO)
func removeChild(parent Context, child canceler) {
    p, ok := parentCancelCtx(parent)
    if !ok {
        return
    }
    p.mu.Lock()
    if p.children != nil {
        delete(p.children, child)
    }
    p.mu.Unlock()
}

接下来分析Cancel相关代码

type CancelFunc func()

//WithCancel方法返回一个继承parent的Context对
//象,同时返回的cancel方法可以用来关闭当前
//Context中的Done channel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}

func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            //如果找到的parent已经被cancel,
            //则将方才传入的child树给cancel掉
            child.cancel(false, p.err)
        } else {
           //否则, 将child节点直接添加到parent的children中
           //(这样就有了约束关系,向上的父亲指针不变
           //,向下的孩子指针可以直接使用 )
            if p.children == nil {
                p.children = make(map[canceler]bool)
            }
           p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
      //如果没有找到最近的可以被cancel的parent,
      //则启动一个goroutine,等待传入的parent终止,
      //并cancel传入的child树,或者等待传入的child终结。
      go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

//判断parent是否为cancelCtx类型
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {
        switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

timerCtx 继承cancelCtx的结构体,这种设计就可以避免写重复代码,提高复用

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) String() string {
    return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
}

// 与cencelCtx有所不同,除了处理cancelCtx.cancel,
// 还回对c.timer进行Stop(),并将c.timer=nil
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    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()
}

timerCtx具体的两个方法:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
   //如果parent的deadline比新传入的deadline早,则直接
   //返回WithCancel,因为parent的deadline会先失效,而新的
   //deadline根据不需要
    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  deadline,
    }
    propagateCancel(parent, c)

    //检查如果已经过期,则cancel新child树
    d := deadline.Sub(time.Now())
    if d <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }

    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        //没有被cancel的话,就设置deadline之后cancel的计时器
        c.timer = time.AfterFunc(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}


// WithTimeout简单暴力,直接把WithTimeout返回
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

valueCtx主要用来传递一些可比较操作的数据

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


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

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

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

《go语言精进之路》读书笔记|理解go语言的设计哲学(代码片段)

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!附上汇总贴:《Go语言精进之路》读书笔记|汇总_COCOgsta的博... 查看详情

go语言之context

...制并发有两种经典的方式,一种是WaitGroup,另外一种就是Context,今天我就谈谈Context。什么是WaitGroupWaitGroup以前我们在并发的时候介绍过,它是一种控制并发的方式,它的这种方式是控制多个goroutine同时完成。 funcmain(){ &n... 查看详情

go语言和javapython等其他语言的对比分析

一、Go语言设计初衷1、设计Go语言是为了解决当时Google开发遇到的问题:大量的C++代码,同时又引入了Java和Python成千上万的工程师数以万计行的代码分布式的编译系统数百万的服务器2、Google开发中的痛点:编译慢失控的依赖每... 查看详情

go将统治下一个10年?go语言发展现状分析

“本文是国内Go语言大中华区首席布道师——许式伟,在QCon2015上海站上的分享。他预测Go语言10年内一定会超过C和java,并且统治这一个10年。Go语言语法及标准库变化Go从1.0版本到现在(2015年)已经有三年多的时间,大的版本发... 查看详情

go语言中的类型及数据结构

...式很多:使用var关键字是最基本的定义变量的方式,与C语言有些不同,如下:varvariable_nametype定义多个变量varname1,name2,name3type定义变量同时初始化varname1string="liming"同时初始化多个变量varname1,name2 查看详情

轻松上手!手把手带你掌握从context到go设计理念

...互启迪共成长。本文作者是腾讯后端开发工程师陈雪锋。context包比较小,是阅读源码比较理想的一个入手,并且里面也涵盖了许多go设计理念可以学习。go 查看详情

轻松上手!手把手带你掌握从context到go设计理念

...互启迪共成长。本文作者是腾讯后端开发工程师陈雪锋。context包比较小,是阅读源码比较理想的一个入手,并且里面也涵盖了许多go设计理念可以学习。go 查看详情

【r语言】解决go富集分析绘图,标签重叠问题

参考技术A前面我给大家详细介绍过☞GO简介及GO富集结果解读☞四种GO富集柱形图、气泡图解读☞GO富集分析四种风格展示结果—柱形图,气泡图☞KEGG富集分析—柱形图,气泡图,通路图☞DAVIDGO和KEGG富集分析及结... 查看详情

go语言导学

1,Go语言简介        Go语言也叫Golang语言,由谷歌公司推出。        2007年9月开始,罗伯特·格瑞史莫,罗勃·派克及肯·汤普逊等大牛开始设计Go语言。        2009年11月,Go语言正式宣布推出࿰... 查看详情

go语言设计模式之函数式选项模式(代码片段)

Go语言设计模式之函数式选项模式本文主要介绍了Go语言中函数式选项模式及该设计模式在实际编程中的应用。为什么需要函数式选项模式?最近看go-micro/options.go源码的时候,发现了一段关于服务注册的代码如下:typeOptionsstructBr... 查看详情

go语言实战抽奖系统

第1章课程介绍课程内容的整体介绍以及学习建议。1-1Go抽奖系统导学第2章6种抽奖活动来一遍看书不如动手,本章将从年会抽奖、彩票刮奖、微信摇一摇、支付宝集福卡、微信抢红包、抽奖大转盘6种抽奖活动的实现出发,让小伙... 查看详情

go语言12(代码片段)

context使用介绍主要功能:控制超时时间保存上下文数据使用context处理超时基本语法结构import""context""//生成和释放定时器ctx,cancel:=context.WithTimeout(context.Background(),2*time.Second)defercancel()//超时控制selectcase<-ctx.Done()://超时时要执行... 查看详情

go语言介绍及环境准备(代码片段)

第一章认识go语言Go语言诞生在2007的某一天,一些大牛正在用c++开发一些比较繁琐但是核心的工作,主要包括庞大的分布式集群,这些大牛就觉得很闹心,后来c++委员会来他们公司演讲,说c++将要添加大概35种新特性。于是其中... 查看详情

大屏设计系列之五——大屏设计语言分析

如果您想订阅本博客内容,每天自动发到您的邮箱中, 请点这里作者:蓝蓝蓝蓝设计经常会接到大屏设计的项目,比如中国移动互联网监控大屏可视化设计及开发、太极集团承接的中央台应急指挥中心大屏可视化设计、交大... 查看详情

go语言gin处理请求参数(代码片段)

Go语言Gin处理请求参数1.获取Get请求参数获取Get请求参数的常用3个函数:func(c*Context)Query(keystring)stringfunc(c*Context)DefaultQuery(key,defaultValuestring)stringfunc(c*Context)GetQuery(keystring)(string,bool)2.获取Post请求参 查看详情

linux后台开发相关面试知识点汇总脑图版

...熟悉Unix/Linux操作系统原理,常用工具、Python/Shell等脚本语言等;熟悉Mysql数据库管理、开发;熟悉redis等NoSQL存储使用;具备良好的分析解决问题能力,能独立承担任务和有系统进度把控能力;责任心强,良好的对外沟通和团队协... 查看详情

为啥要学习golang?

Go语言其实是Golanguage的简称,Go(又称Golang)是Google的RobertGriesemer,RobPike及KenThompson开发的一种静态强类型、编译并发型语言。Go语言语法与C相近,但功能上有:内存安全,GC(垃圾回收),结构形态及CSP-style并发计算。该语言... 查看详情

09.go语言并发(代码片段)

Go语言并发并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。本章讲解的并发含义属于多线程编程。Go语言通过编译器运行时(runtime),从语言上支持了并发的特性... 查看详情