Go Context 使用和代码分析(好理解)

news/2024/9/23 17:01:45
 

Go语言中的go-routine是go语言中的最重要的一部分,是一个用户级的线程是Go语言实现高并发高性能的重要原因.但是如何停止一个已经开启的go-routine呢?一般有几种方法:

  • 使用共享内存来停止go-routine,比如通过判断一个全局变量来判断是否要停止go-routine
  • 使用文件系统来停止go-routine,跟使用内存相同用文件来判断
  • 使用context上下文,context也是大家最推荐的一种方式.并且可以结束嵌套的go-routine.

简单使用

context库中,有4个关键方法:

  • WithCancel 返回一个cancel函数,调用这个函数则可以主动停止go-routine.
  • WithValue WithValue可以设置一个key/value的键值对,可以在下游任何一个嵌套的context中通过key获取value.但是不建议使用这种来做go-routine之间的通信.
  • WithTimeout 函数可以设置一个time.Duration,到了这个时间则会cancel这个context.
  • WithDeadline WithDeadline函数跟WithTimeout很相近,只是WithDeadline设置的是一个时间点.
package mainimport ("context""fmt""time"
)func main() {//cancelctx, cancel := context.WithCancel(context.Background())go work(ctx, "work1")time.Sleep(time.Second * 3)cancel()time.Sleep(time.Second * 1)// with valuectx1, valueCancel := context.WithCancel(context.Background())valueCtx := context.WithValue(ctx1, "key", "test value context")go workWithValue(valueCtx, "value work", "key")time.Sleep(time.Second * 3)valueCancel()// timeoutctx2, timeCancel := context.WithTimeout(context.Background(), time.Second*3)go work(ctx2, "time cancel")time.Sleep(time.Second * 5)timeCancel()// deadlinectx3, deadlineCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3))go work(ctx3, "deadline cancel")time.Sleep(time.Second * 5)deadlineCancel()time.Sleep(time.Second * 3)}func workWithValue(ctx context.Context, name string, key string) {for {select {case <-ctx.Done():fmt.Println(ctx.Value(key))println(name, " get message to quit")returndefault:println(name, " is running", time.Now().String())time.Sleep(time.Second)}}
}func work(ctx context.Context, name string) {for {select {case <-ctx.Done():println(name, " get message to quit")returndefault:println(name, " is running", time.Now().String())time.Sleep(time.Second)}}
}

代码分析

context的原理其实就是利用了channel struct{}的特性,使用select获取channel数据.一旦关闭这个channel则会收到数据退出go-routine中的逻辑.context也是支持嵌套使用,结构就如下图显示利用的是一个map类型来存储子context.关闭一个节点就会循环关闭这个节点下面的所有子节点,就实现了优雅的退出go-routine的功能.下面我们看具体接口对象和源码逻辑.

tech.mojotv.cn_图片描述

Context 核心方法和结构体说明

context interface 有4个方法

  • Deadline 该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值
  • Done 返回一个channel.当timeout或者调用cancel方法时,将会close掉
  • Err 返回一个Error
  • Value 返回WithValue设置的值
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}

emptyCtx

在上面的例子中我们可以看到函数context.Background(), 这个函数返回的就是一个emptyCtx

emptyCtx经常被用作在跟节点或者说是最上层的context,因为context是可以嵌套的.在上面的Withvalue的例子中已经看到,先用emptyCtx创建一个context,然后再使用withValue把之前创建的context传入.这个操作会在下面的分析中详细了解的.

下面就是emptyCtx,其实实现很简单所有的方法几乎返回的都是nil.

ToDo函数返回的也是

var (background = new(emptyCtx)todo       = new(emptyCtx)
)type emptyCtx intfunc (*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"
}var (background = new(emptyCtx)todo       = new(emptyCtx)
)func Background() Context {return background
}func TODO() Context {return todo
}

cancelCtx

cancelCtx是context实现里最重要的一环,context的取消几乎都是使用了这个对象.WithDeadline WithTimeout其实最终都是调用的cancel的cancel函数来实现的.

对象中的字段:

  • Context 保存parent Context
  • mu 用来保护数据
  • done 用来标识是否已被cancel.当外部触发cancel,或者父Context的channel关闭时,此done也会关闭
  • children 保存它的所有子canceler
  • err 已经cancel则err!= nil cancel主要函数:

Done

Done函数返回一个chan struct{}的channel,用来判断context是否已经被close了.从上面的例子可以看到使用一个select 来判断context是否被关闭.一旦从外部调用cancel函数关闭了context的done属性,select则可以拿到输出,最终关闭这个context

Cancel

Cancel函数用来在外部调用,调用之后主要操作:

  1. 加锁避免多出操作
  2. 如果cancelCtx的done未被初始化则初始化一个(这个属于lazyload)
  3. 调用close(c.done) 来关闭channel,由于make(chan struct{})的特性,上面的Done channel则会接收到数据
  4. 循环调用context.children 的cancel方法,关闭所有嵌套的context.
  5. 释放锁c.mu.Unlock()
  6. 根据参数removeFromParent来判断是否要
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     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 call
}// 可以被cancel的对象,实现者是*cancelCtx 和 *timerCtx.
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}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)
}// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) C(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 // already canceled}c.err = errif c.done == nil {c.done = closedchan} else {close(c.done)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}

timerCtx

timeCtx其实是在cancelCtx基础上增加timer属性.其中的cancel函数也是调用cancelCtx的Cancel函数.

type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}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()
}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}

WithCancelWithDeadlineWithTimeoutWithValue

这三个方法是对于context使用的一个封装,在最上边的例子里我们可以看到是如何使用的.在这段我们是要看的是如何实现的源码.

WithCancel

WithCancel函数返回context和一个主动取消的函数,外部只要调用这个函数则会close context中channel.

返回的函数测试cancelCtx中测cancel函数,在上面已经有了详细说明这里就不过多描述了.

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}

WithDeadline

  1. 判断父节点中的deadline是否比父节点的早,如果是则直接调用WithCancel
  2. 创建一个timerCtx,timerCtx的具体描述也在上面详细分析过了
  3. 使用time.afterFunc设置dur,当时间到了则执行timerCtx.Cancel最终执行的也是cancelCtx.Cancel
  4. 返回Cancel函数,方便外部调用
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(true, Canceled) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}

WithTimeout

WithTimeout实现很简单,其实就是调用了WithDeadline方法,传入已经计算过的deadline.

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

WithValue

WithValue 不返回cancel函数,只是把传入的key和value保存起来.方便上下游节点根据key获取value.

type valueCtx struct {Contextkey, 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)
}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}
}

使用原则

从网上看到了一些使用原则,把他摘抄下来:

  • 不要把Context存在一个结构体当中,显式地传入函数.Context变量需要作为第一个参数使用,一般命名为ctx.
  • 即使方法允许,也不要传入一个nil的Context,如果您不确定您要用什么Context的时候传一个context.TODO.
  • 使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数.
  • 同样的Context可以用来传递到不同的go-routine中,Context在多个go-routine中是安全的

总结

上面讲述了context的用法和源码,其实有很多框架都实现了自己的context.其实只要继承了context接口就是一个context对象.Context是大家都比较推荐的一种停止go-routine的一种方式,并且context支持嵌套,停止跟节点它下面所有的子节点都会停止.

类别和标签

TCP/IP notes CPP CheatSheet 快速入门 快速查询 Jekyll Github Python Golang Shell Bash docker 教程 container nginx dockerfile golang Spider 钉钉 PhantomJS RPC protobuf gRPC 文件 操作 GUI reflect 网络请求 代理 http chromedp Go进阶 工具 Vim Javascript seaweedfs Raft raft Git Nginx RESTful gitbook node npm github pages toml lumen laravel cors markdown 日志 log mysql 定时任务 php 程序员 Python教程 redis Dockerhub Docker OSS 网络协议 python 运维开发 kali linux 树莓派 raspberryPi algorithm 算法 黑客 hack ssh xtermjs shell context gcc g++ jekyll 破解 LDAP Linux 有声读物 CSS ES6 OAuth2 Prometheus Grafana Nes Game HTTP3 Java 动手 Gateway 网关 Rust笔记 fullstack chrome
 

未找到相关的 Issues 进行评论

请联系 @mojocn 初始化创建

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

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

相关文章

代理模式 - 动态代理

动态代理的APIProxy 动态代理类生成代理对象:Proxy.newProxyInstance( 类加载器,接口数组,处理器 )类加载器:对象.getClass( ).getClassLoader( ) 接口数组-被代理类的所有接口:被代理对象.getClass( ).getInterfaces( ) 处理器:代理对象调用方法时,会被处理器拦截Invoc…

9.23制作二维码

二维码在教育领域的应用日益广泛,如在线教育、校园导览等。学生可以通过扫描二维码,获取课程资料、校园地图等信息。这个海报上的二维码是连接到课文我变成了一棵树,直接看到文字内容,方便学生学习。

高级语言程序设计课程第一次个人作业 102400226 石华波

2024高级语言程序设计:https://edu.cnblogs.com/campus/fzu/2024C 高级语言程序设计课程第一次个人作业:https://edu.cnblogs.com/campus/fzu/2024C/homework/13264 学号:102400226 姓名:石华波

专为工程地质领域安全监测而设计,BWII型广播预警遥测系统助您实现全面监测!

专为工程地质领域安全监测而设计,BWII型广播预警遥测系统助您实现全面监测!BWII型广播预警遥测系统是一款新型的雨量预警监测仪,具备多通道和多类型传感器接入功能。该系统能够定时采集和发送电压、电流、数字和脉冲等信息,同时结合事件驱动的工作方式,以高频传感扫描和定…

2024 ByteCTF

ByteCTF 极限逃脱 题目描述:本题需要通过动态调试分析出要输入的内容,可能在某些地方会有提示出现。 这是一个IOS逆向,因为没有设备只能静态分析 流程和安卓逆向大概一致 解压拖进ida 提示输入flag格式 根据"-"进行切割其实就是uuid格式,正确输入后有一个赋值操…

网络流学习记录

CCPC网络赛 G Problem G. 疯狂星期六 Input file: standard input    Output file: standard output Time limit: 1 second      Memory limit: 256 megabytes yyq 和他的朋友们一共 n 个人(编号为 1 到 n ,yyq 编号为 1)去某饭店吃疯狂星期六。第 i 个人初始手中有 a…

PARTIII-Oracle事务管理-事务

10. 事务 10.1. 事务简介 事务是包含一个或多个SQL语句的逻辑、原子工作单元。事务将SQL语句分组,使它们要么全部提交,这意味着它们被应用到数据库中,要么全部回滚,这意味着它们从数据库中被撤销。Oracle数据库为每个事务分配一个唯一的标识符,称为事务ID。 所有Oracle事务…