热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

实用的context

context是什么context翻译过来是上下文的意思,在go里面是用来给goroutine传递上下文的,可以传递取消信号、超时、k-v等等,比如说我们经常用它来存储客户端传过来






context是什么

context翻译过来是上下文的意思,在go里面是用来给goroutine传递上下文的,可以传递取消信号、超时、k-v等等,比如说我们经常用它来存储客户端传过来的原子参数,token、COOKIE等等


context的使用

上文说了,context可以传递cv,设置goroutine取消、超时等,我们用一些简单代码来看看context是怎么实现这些功能的


传递k-v

func TestCommon(t *testing.T) {
ctx := setTokenContext(context.Background())
wg := new(sync.WaitGroup)
wg.Add(2)
go run(ctx, wg, 1)
go run(ctx, wg, 2)
wg.Wait()
}
func getToken(ctx context.Context) string {
return ctx.Value("token").(string) // 从context中获取token,因为value方法取出来是interface{},所以需要类型断言成string
}
func setTokenContext(ctx context.Context) context.Context{
return context.WithValue(ctx, "token", "abc-abc-abc") // context中设置token
}
func run(ctx context.Context, wg *sync.WaitGroup, num int) {
//todo 业务
defer wg.Done()
token := getToken(ctx)
fmt.Printf("这里是第%d个goroutian, token为:%s \n", num, token)
}

上面这段代码我们现创建了一个context,然后通过WithValue函数往context中存储了token数据,然后开启两个groutine去获取token,最终的打印结果为下图


关闭goroutine

通常我们可以通过context发送一个信号来关闭我们需要关闭的goroutine

func TestCommon(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go run(ctx, 1) //goroutian1
go run(ctx, 2) //goroutian2
time.Sleep(time.Second)
cancel() // 发送取消信号
time.Sleep(time.Second)
}
func run(ctx context.Context, num int) {
for {
select {
case <- ctx.Done():
fmt.Printf("groutine%d被关闭了\n", num)
return
default:
//执行正常逻辑
fmt.Printf("groutine%d正在执行\n", num)
}
time.Sleep(time.Second) // 每秒钟执行一次
}
}

先调用了一下WithCancel,可以看到函数返回了一个context,还有一个cancel函数,这个cancel函数其实就是发送一个信号给ctx.Done()返回的channel,所以我们通过监听这个channel就可以友好的关闭goroutine,具体的实现逻辑等下文源码解析时会说

打印结果

实用的context


控制超时

可以使用context来控制groutian的执行时间,超过某个时间就关闭掉,例如http超时,在golang里就是通过context实现的,先看示例代码:

func TestCommon(t *testing.T) {
//ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10 * time.Second))
ctx, _ := context.WithTimeout(context.Background(), time.Second * 3)
go run(ctx) //goroutian1
time.Sleep(5 * time.Second)
}
func run(ctx context.Context) {
for {
select {
case <- ctx.Done():
fmt.Printf("groutine超时被关闭了\n")
return
default:
//执行正常逻辑
fmt.Printf("groutine正在执行\n")
}
time.Sleep(time.Second) // 每秒钟执行一次
}
}

先调用了WithTimeout函数,返回两个数据,第一个是context,第二个和上面取消的逻辑一样,也会返回一个cancel函数,所以,也是可以手动cancel的,这里主要说超时,所以先把它过滤掉。WithTimeout函数第二个参数是个时间,time.xx,这里可以传秒、分钟、小时等等等,传多少时间进去,代表我们希望groutine多长时间超时,超时以后和上文提到的手动取消一样,也是发送一个信号给ctx.Done()返回的channel

注意:代码中注释了一行context.WithDeadline其实它和WithTimeout的执行是完全一样的,就是参数不一样而已,这个一会解析源码的时候也会说
打印结果:

实用的context


原理解析

先看下context源码包,位于$GOPATH/src/context文件夹下,context.go,很简单,总共500多行代码,注释就有200多行


接口


context

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

Context 是一个接口,定义了 4 个方法

Done() 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们知道,读一个已关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面放入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。

Err() 返回一个错误,表示 channel 被关闭的原因。例如是被取消,还是超时。

Deadline() 返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。

Value() 获取之前设置的 key 对应的 value。


canceler

再来看另一个接口

type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:cancelCtx 和 timerCtx。


结构体


emptyCtx

源码中给出了一个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
}

这段代码太简单了,一个emptyCtx实现了Context接口,并且每个函数都返回都是空的这是一个空的Context,不会被cancel,不会用来储存值,源码把它包装成了两个对象

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}

Background 通常用在 main 函数中,作为所有 context 的根节点。TODO一般是在那种我们不知道要传什么ctx时,作为一个占位符来使用


valueCtx

type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
//创建一个带key-value的context
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

valueCtx的结构很简单,匿名字段Context,还有key,value为两个interface用来存储值
先看一下获取valueCtx的方法WithValue,函数有三个参数,刚好和结构体对应,比如我们上面示例代码那块写的ctx := context.WithValue(context.Background(), "token", "abc"),这个background就是parent,然后赋值给结构体的第一个参数,最后返回。我们发现ctx是没有其他结构类似SetValue这种可以设置key和值的方法的,那假如说我想再往里面增加一条数据怎么办呢,我们只能像这样:

ctx := context.WithValue(context.Background(), "a", 1)
ctx = context.WithValue(ctx, "b", 2)

这时候两个ctx变量已经不是同一个context了,第一个ctx变成了第二个ctx的parent,那这个时候第二个ctx是个什么结构,根据上面的代码,生成的ctx结构应该是这样的

valueCtx {
parent: {
parent:emptyCtx //background
key: "a",
value: 1,
}
key:"b",
value:2
}

很像一个倒着的链表,用parent来关联父级,现在再来看看Value函数,代码很简单,判断key是否和传过来的key相等,如果相等直接返回,否则调用父级的value方法,这里相当于递归寻找,直到最后找到background根节点。


cancelCtx

我们再开看可取消的context实现

type cancelCtx struct {
Context
mu sync.Mutex // 用来加锁
done chan struct{} // 接收关闭信号的channel
children map[canceler]struct{} // 所有的子节点存为一个map
err error // 保存被取消以后的取消原因,cancel或者超时
}

它实现了canceler接口,是一个可取消的context,由于里面也含有一个Context匿名字段,所以可以被看作是Context类型,先来看看怎么创建它

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}
}

代码很简单,初始化一个cancelCtx,和valueCtx一样,把parent赋值给了匿名属性Context,然后返回一个Context和cancel函数,主要看一下propagateCancel函数做了些什么

func propagateCancel(parent Context, child canceler) {
// 获取父级的done,判断父级是否含有cancelCtx
// 如果父级无cancelCtx,直接返回
// 否则使用map增加关联关系,方便将来cancel父级的时候把子级一起cancel
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 如果父级已经取消了,取消当前ctx
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 寻找父级cancelCtx(递归查找)
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
// 核心!!!!!父级的children中保存当前ctx
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

其实这段代码的核心就两点,判断是否有上级,并且是cancelCtx类型,如果没有,什么也不用做,如果有的话就用上级的children保存当前ctx,方便将来级联取消。

接下来我们看看cancel函数

// 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) 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 // already canceled
}
c.err = err
// 获取ctx的done(是个channel),然后关闭这个channel
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 获取下级context,级联取消
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()
if removeFromParent {
removeChild(c.Context, c)
}
}

这段代码的核心其实就是close这个ctx中的done,还记得上文中提到我们怎么监听协程是否需要关闭:

case <- ctx.Done():

通过监听ctx.Done(),如果拿到值就代表被cancel了,当调用close时,这边的监听就可以拿到数据了。

最后,让我们看下timeCtx的实现

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// 如果父级也是时间类型的ctx,判断超时时间是否大于父级的超时时间,如果大于其实是不需要生成新的ctx,上文有讲,因为父级超时时间到了会直接调用cancel,把子级也一起取消。
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 生成时间类型ctx,并设置cancelCtx属性
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 上文有专门针对这个函数的讲解
propagateCancel(parent, c)
dur := time.Until(d)
// 如果超时时间小于0,直接取消
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()
if c.err == nil {
// 重点!!!!!!!开启定时器,并且到时调用cancel函数
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

代码很简单,如果理解了上面cancel类型的context,那这个无非就是加了一个定时器,到时自动触发cancel函数而已。




推荐阅读
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • springmvc学习笔记(十):控制器业务方法中通过注解实现封装Javabean接收表单提交的数据
    本文介绍了在springmvc学习笔记系列的第十篇中,控制器的业务方法中如何通过注解实现封装Javabean来接收表单提交的数据。同时还讨论了当有多个注册表单且字段完全相同时,如何将其交给同一个控制器处理。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
author-avatar
了不起的老狐_226
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有