精华内容
下载资源
问答
  • context
    千次阅读
    2022-03-09 15:44:45

    一.Context概述

    1.Context是一个抽象类,其通用实现在ContextImpl类中。它的主要作用是一个访问application环境全局信息的接口,包括为Activities, Fragments, and Services提供访问resource files, images, themes/styles等相关的类,其具体结构类图如下:

    二.Context使用

    1.启动Activity

    1).java方式

    Intent intent = new Intent(context, MyActivity.class);
    startActivity(intent);

    2).kotlin方式

    val intent = Intent(context, MyActivity::class.java)
    startActivity(intent)

    2.创建View,

    1).java方式

    TextView textView = new TextView(context);

    2).kotlin方式

    val textView = TextView(context)

    此外Contexts也包含view需要的一些信息,eg:

    1‘. 将dp、sp 转换为像素的设备屏幕尺寸和维度
    2'. styled属性
    3'. activity onclick关联的属性

    3.Inflating一个XML布局文件

    1).java方式

    LayoutInflater inflater = LayoutInflater.from(context);
    inflater.inflate(R.layout.my_layout, parent);

    2).kotlin方式

    val inflater = LayoutInflater.from(context)
    inflater.inflate(R.layout.my_layout, parent)

    4.发送广播

    1).java方式

    Intent broadcastIntent = new Intent("action");
    LocalBroadcastManager.getInstance(context).sendBroadcast(broadcastIntent);

    2).kotlin方式

    val broadcastIntent = Intent("action")
    LocalBroadcastManager.getInstance(context).sendBroadcast(broadcastIntent)

    5.获取系统Service,eg:发送通知,通知管理获取

    1).java方式

    NotificationManager notificationManager = 
        (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    
    int notificationId = 1;
    
    // Context is required to construct RemoteViews
    Notification.Builder builder = 
        new Notification.Builder(context).setContentTitle("title");
    
    notificationManager.notify(notificationId, builder.build());

    2).kotlin方式

    val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
    val notificationId = 1
    
    // Context is required to construct RemoteViews
    val builder = Notification.Builder(context).setContentTitle("title")
    
    notificationManager.notify(notificationId, builder.build())

    6.Application和Activity Context对比

    在传递Context参数的时候,如果是在Activity中,我们可以传递this(这里的this指的是Activity.this,是当前Activity的context)或者Activity.this。这个时候如果我们传入getApplicationContext(),我们会发现这样也是可以用的。可是大家有没有想过传入Activity.this和传入getApplicationContext()的区别呢?首先Activity.this和getApplicationContext()返回的不是同一个对象,一个是当前Activity的实例,一个是项目的Application的实例,这两者的生命周期是不同的,它们各自的使用场景不同,this.getApplicationContext()取的是这个应用程序的Context,它的生命周期伴随应用程序的存在而存在;而Activity.this取的是当前Activity的Context,它的生命周期则只能存活于当前Activity,这两者的生命周期是不同的。getApplicationContext() 生命周期是整个应用,当应用程序摧毁的时候,它才会摧毁;Activity.this的context是属于当前Activity的,当前Activity摧毁的时候,它才摧毁。

    7.Context的应用场景

    ApplicationActivityServiceContentProviderBroadcastReceiver
    New a DialogNOYESNONONO
    Start ActivityNO1YESNO1NO1NO1
    Inflation LayoutNO2YESNO2NO2NO2
    Start ServiceYESYESYESYESYES
    Bind ServiceYESYESYESYESNO
    Send BroadcastYESYESYESYESYES
    Register BroadcastReceiverYESYESYESYESNO3
    Load ResourceYESYESYESYESYES

    大家注意看到有一些NO后面添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?解释如下:

    NO1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。
    NO2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。
    NO3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。

    8.Context的数量计算

    Context个数=Activity数+Service数+1(Application)

    三.Context内存泄露问题

    1.单例模式导致内存泄漏

    1).java demo

    public class CustomManager {
        private static CustomManager sInstance;
    
        public static CustomManager getInstance(Context context) {
            if (sInstance == null) {
    
                // This class will hold a reference to the context
                // until it's unloaded. The context could be an Activity or Service.
                sInstance = new CustomManager(context);
            }
    
            return sInstance;
        }
    
        private Context mContext;
    
        private CustomManager(Context context) {
            mContext = context;
        }
    }

    2).kotlin demo

    class CustomManager private constructor(private val context: Context) {
        companion object {
            @Volatile
            private var instance: CustomManager? = null
    
            fun getInstance(context: Context): CustomManager {
                val i = instance
                if (i != null) {
                    return instance as CustomManager
                }
                return synchronized(this) {
                    val i2 = instance
                    if (i2 != null) {
                        i2
                    } else {
                        val created = CustomManager(context)
                        instance = created
                        created
                    }
                }
            }
        }
    }

    上面demo有内存泄露的隐患,如果是在Activity中创建这个单例的话,传入的context为Activity的context,如果想要销毁Activity,但是单例的生命周期是整个应用,导致Activity的内存不能完全释放,

    正确的方法是将application context存储在CustomManager.getInstance()中。 application context是一个单例,并且与应用程序进程的生命周期相关联,因此可以安全地存储对它的引用。

    如果在组件的生命周期之外需要Context引用,或者它应该独立于传入的Context的生命周期,请使用application context。eg:

    1).java方式

    public static CustomManager getInstance(Context context) {
        if (sInstance == null) {
    
            sInstance = new CustomManager(context.getApplicationContext());
        }
    
        return sInstance;
    }

    2).kotlin方式

    class CustomManager private constructor(private val context: Context) {
        companion object {
            fun getInstance(context: Context): CustomManager {
    
                val created = CustomManager(context.applicationContext)
            }
        }
    }

    更多相关内容
  • Context介绍

    千次阅读 2022-03-14 20:45:11
    目录Context设计原理默认上下文取消信号传值方法小结 Context 上下文 context.Context Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的...

    Context

    上下文 context.Context Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。

    context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:

    1. Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
    2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
    3. Err — 返回context.Context结束的原因,它只会在Done方法对应的 Channel 关闭时返回非空的值;
      1. 如果 context.Context 被取消,会返回 Canceled 错误;
      2. 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
    4. Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
    type Context interface {
    	Deadline() (deadline time.Time, ok bool)
    	Done() <-chan struct{}
    	Err() error
    	Value(key interface{}) interface{}
    }
    

    context 包中提供的 context.Backgroundcontext.TODOcontext.WithDeadlinecontext.WithValue 函数会返回实现该接口的私有结构体,我们会在后面详细介绍它们的工作原理。

    设计原理

    **在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。**Go 服务的每一个请求都是通过单独的 Goroutine 处理的2,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

    如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。

    image-20220310222737660

    每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

    image-20220313213745497

    如上图所示,当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:

    image-20220313213802561

    我们可以通过一个代码片段了解 context.Context 是如何对信号进行同步的。在这段代码中,我们创建了一个过期时间为 1s 的上下文,并向上下文传入 handle 函数,该方法会使用 500ms 的时间处理传入的请求:

    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    	defer cancel()
    
    	go handle(ctx, 500*time.Millisecond)
    	select {
    	case <-ctx.Done():
    		fmt.Println("main", ctx.Err())
    	}
    }
    
    func handle(ctx context.Context, duration time.Duration) {
    	select {
    	case <-ctx.Done():
    		fmt.Println("handle", ctx.Err())
    	case <-time.After(duration):
    		fmt.Println("process request with", duration)
    	}
    }
    

    因为过期时间大于处理时间,所以我们有足够的时间处理该请求,运行上述代码会打印出下面的内容:

    $ go run context.go
    process request with 500ms
    main context deadline exceeded
    

    handle 函数没有进入超时的 select 分支,但是 main 函数的 select 却会等待 context.Context 超时并打印出 main context deadline exceeded

    如果我们将处理请求时间增加至 1500ms,整个程序都会因为上下文的过期而被中止,:

    $ go run context.go
    main context deadline exceeded
    handle context deadline exceeded
    

    相信这两个例子能够帮助各位读者理解 context.Context 的使用方法和设计原理 — 多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

    默认上下文

    context 包中最常用的方法还是 context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 backgroundtodo,它们会在同一个 Go 程序中被复用:

    func Background() Context {
    	return background
    }
    
    func TODO() Context {
    	return todo
    }
    

    这两个私有变量都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 context.emptyCtx 的指针,这是最简单、最常用的上下文类型:

    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
    }
    

    从上述代码中,我们不难发现 context.emptyCtx 通过空方法实现了 context.Context 接口中的所有方法,它没有任何功能。

    image-20220313214905601

    从源代码来看,context.Backgroundcontext.TODO 也只是互为别名,没有太大的差别,只是在使用和语义上稍有不同:

    • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
    • context.TODO 应该仅在不确定应该使用哪种上下文时使用;

    在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

    取消信号

    context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

    image-20220313215304053

    我们直接从 context.WithCancel 函数的实现来看它到底做了什么:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    	c := newCancelCtx(parent)
    	propagateCancel(parent, &c)
    	return &c, func() { c.cancel(true, Canceled) }
    }
    
    func propagateCancel(parent Context, child canceler) {
    	done := parent.Done()
    	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 {
    			p.children[child] = struct{}{}
    		}
    		p.mu.Unlock()
    	} else {
    		go func() {
    			select {
    			case <-parent.Done():
    				child.cancel(false, parent.Err())
    			case <-child.Done():
    			}
    		}()
    	}
    }
    

    上述函数总共与父上下文相关的三种不同的情况:

    1. parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数会直接返回;
    2. 当child的继承链包含可以取消的上下文时,会判断parent 是否已经触发了取消信号;
      • 如果已经被取消,child 会立刻被取消;
      • 如果没有被取消,child 会被加入 parentchildren 列表中,等待 parent 释放取消信号;
    3. 当父上下文是开发者自定义的类型、实现了context.Context接口并在Done()方法中返回了非空的管道时;
      1. 运行一个新的 Goroutine 同时监听 parent.Done()child.Done() 两个 Channel;
      2. parent.Done() 关闭时调用 child.cancel 取消子上下文;

    context.propagateCancel 的作用是在 parentchild 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会出现状态不一致的情况。

    context.cancelCtx 实现的几个接口方法也没有太多值得分析的地方,该结构体最重要的方法是 context.cancelCtx.cancel,该方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号:

    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    	c.mu.Lock()
    	if c.err != nil {
    		c.mu.Unlock()
    		return
    	}
    	c.err = err
    	if c.done == nil {
    		c.done = closedchan
    	} else {
    		close(c.done)
    	}
    	for child := range c.children {
    		child.cancel(false, err)
    	}
    	c.children = nil
    	c.mu.Unlock()
    
    	if removeFromParent {
    		removeChild(c.Context, c)
    	}
    }
    

    除了 context.WithCancel 之外,context 包中的另外两个函数 context.WithDeadlinecontext.WithTimeout 也都能创建可以被取消的计时器上下文 context.timerCtx

    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) {
    	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    		return WithCancel(parent)
    	}
    	c := &timerCtx{
    		cancelCtx: newCancelCtx(parent),
    		deadline:  d,
    	}
    	propagateCancel(parent, c)
    	dur := time.Until(d)
    	if dur <= 0 {
    		c.cancel(true, DeadlineExceeded) // 已经过了截止日期
    		return c, func() { c.cancel(false, 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) }
    }
    

    context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。

    context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能:

    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) cancel(removeFromParent bool, err error) {
    	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()
    }
    

    context.timerCtx.cancel 方法不仅调用了 context.cancelCtx.cancel,还会停止持有的定时器减少不必要的资源浪费。

    传值方法

    在最后我们需要了解如何使用上下文传值,context 包中的 context.WithValue 能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtx 类型:

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

    context.valueCtx 结构体会将除了 Value 之外的 ErrDeadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法,该方法的实现也很简单:

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

    如果 context.valueCtx 中存储的键值对与 context.valueCtx.Value 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。

    小结

    Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到。

    在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 传递请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

    展开全文
  • Go context详解

    千次阅读 多人点赞 2020-09-04 19:45:29
    什么是 context Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。 context 主要用来在 goroutine 之间传递上下文信息,包括:取消...

    什么是 context

    Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

    context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

    随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。

    context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
    与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。

    为什么有 context

    Go 常用来写后台服务,通常只需要几行代码,就可以搭建一个 http server。

    在 Go 的 server 里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据……
    在这里插入图片描述
    这些 goroutine 需要共享这个请求的基本数据,例如登陆的 token,处理请求的最大超时时间(如果超过此值再返回数据,请求方因为超时接收不到)等等。当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源。

    再多说一点,Go 语言中的 server 实际上是一个“协程模型”,也就是说一个协程处理一个请求。例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会越来越多。而我们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外表现为不可用,这肯定是 P0 级别的事故。这时,肯定有人要背锅了。

    其实前面描述的 P0 级别事故,通过设置“允许下游最长处理时间”就可以避免。例如,给下游设置的 timeout 是 50 ms,如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默认值或者错误。例如,返回商品的一个默认库存数量。注意,这里设置的超时时间和创建一个 http client 设置的读写超时时间不一样,这里不详细展开。可以去看看参考资料【Go 在今日头条的实践】一文,有很精彩的论述。

    context 包就是为了解决上面所说的这些问题而开发的:在 一组 goroutine 之间传递共享的值、取消信号、deadline……

    在这里插入图片描述
    用简练一些的话来说,在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。

    一句话:context 用来解决 goroutine 之间退出通知、元数据传递的功能。

    context 底层实现原理

    我们分析的 Go 版本依然是 1.9.2。

    整体概览

    context 包的代码并不长,context.go 文件总共不到 500 行,其中还有很多大段的注释,代码可能也就 200 行左右的样子,是一个非常值得研究的代码库。

    先给大家看一张整体的图:
    在这里插入图片描述
    在这里插入图片描述

    上面这张表展示了 context 的所有函数、接口、结构体,可以纵览全局,可以在读完文章后,再回头细看。

    整体类图如下:

    在这里插入图片描述

    接口

    Context

    现在可以直接看源码:

    type Context interface {
        // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
        Done() <-chan struct{}
    
        // 在 channel Done 关闭后,返回 context 取消原因
        Err() error
    
        // 返回 context 是否会被取消以及自动取消时间(即 deadline)
        Deadline() (deadline time.Time, ok bool)
    
        // 获取 key 对应的 value
        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。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。

    Context 接口设计成这个样子的原因:

    • “取消”操作应该是建议性,而非强制性

    caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。

    • “取消”操作应该可传递

    “取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。

    结构体

    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
    }
    

    看这段源码,非常 happy。因为每个函数都实现的异常简单,要么是直接返回,要么是返回 nil。

    所以,这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。

    它被包装成:

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

    通过下面两个导出的函数(首字母大写)对外公开:

    func Background() Context {
        return background
    }
    
    func TODO() Context {
        return todo
    }
    

    background 通常用在 main 函数中,作为所有 context 的根节点。

    todo 通常用在并不知道传递什么 context的情形。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。

    cancelCtx

    再来看一个重要的 context:

    type cancelCtx struct {
        Context
    
        // 保护之后的字段
        mu       sync.Mutex
        done     chan struct{}
        children map[canceler]struct{}
        err      error
    }
    

    这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

    先来看 Done() 方法的实现:

    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
    }
    

    c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

    Err()String() 方法比较简单,不多说。推荐看源码,非常简单。

    接下来,我们重点关注 cancel() 方法的实现:

    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
        // 必须要传 err
        if err == nil {
            panic("context: internal error: missing cancel error")
        }
        c.mu.Lock()
        if c.err != nil {
            c.mu.Unlock()
            return // 已经被其他协程取消
        }
        // 给 err 字段赋值
        c.err = err
        // 关闭 channel,通知其他协程
        if c.done == nil {
            c.done = closedchan
        } else {
            close(c.done)
        }
    
        // 遍历它的所有子节点
        for child := range c.children {
            // 递归地取消所有子节点
            child.cancel(false, err)
        }
        // 将子节点置空
        c.children = nil
        c.mu.Unlock()
    
        if removeFromParent {
            // 从父节点中移除自己 
            removeChild(c.Context, c)
        }
    }
    

    总体来看,cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。

    我们再来看创建一个可取消的 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}
    }
    

    这是一个暴露给用户的方法,传入一个父 Context(这通常是一个 background,作为根节点),返回新建的 context,新 context 的 done channel 是新建的(前文讲过)。

    当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。

    注意传给 WithCancel 方法的参数,前者是 true,也就是说取消的时候,需要将自己从父节点里删除。第二个参数则是一个固定的取消错误类型:

    var Canceled = errors.New("context canceled")
    

    还注意到一点,调用子节点 cancel 方法的时候,传入的第一个参数 removeFromParent 是 false。

    两个问题需要回答:1. 什么时候会传 true?2. 为什么有时传 true,有时传 false?

    removeFromParent 为 true 时,会将当前节点的 context 从父节点 context 中删除:

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

    最关键的一行:

    delete(p.children, child)
    

    什么时候会传 true 呢?答案是调用 WithCancel() 方法的时候,也就是新创建一个可取消的 context 节点时,返回的 cancelFunc 函数会传入 true。这样做的结果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点,你自己取消了,所以我要和你断绝关系,对其他人没影响。

    在取消函数内部,我知道,我所有的子节点都会因为我的一:c.children = nil 而化为灰烬。我自然就没有必要再多做这一步,最后我所有的子节点都会和我断绝关系,没必要一个个做。另外,如果遍历子节点的时候,调用 child.cancel 函数传了 true,还会造成同时遍历和删除一个 map 的境地,会有问题的。
    在这里插入图片描述
    如上左图,代表一棵 context 树。当调用左图中标红 context 的 cancel 方法后,该 context 从它的父 context 中去除掉了:实线箭头变成了虚线。且虚线圈框出来的 context 都被取消了,圈内的 context 间的父子关系都荡然无存了。

    重点看 propagateCancel()

    func propagateCancel(parent Context, child canceler) {
        // 父节点是个空节点
        if parent.Done() == nil {
            return // parent is never canceled
        }
        // 找到可以取消的父 context
        if p, ok := parentCancelCtx(parent); ok {
            p.mu.Lock()
            if p.err != nil {
                // 父节点已经被取消了,本节点(子节点)也要取消
                child.cancel(false, p.err)
            } else {
                // 父节点未取消
                if p.children == nil {
                    p.children = make(map[canceler]struct{})
                }
                // "挂到"父节点上
                p.children[child] = struct{}{}
            }
            p.mu.Unlock()
        } else {
            // 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
            go func() {
                select {
                case <-parent.Done():
                    child.cancel(false, parent.Err())
                case <-child.Done():
                }
            }()
        }
    }
    

    这个方法的作用就是向上寻找可以“挂靠”的“可取消”的 context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。

    这里着重解释下为什么会有 else 描述的情况发生。else 是指当前节点 context 没有向上找到可以取消的父节点,那么就要再启动一个协程监控父节点或者子节点的取消动作。

    这里就有疑问了,既然没找到可以取消的父节点,那 case <-parent.Done() 这个 case 就永远不会发生,所以可以忽略这个 case;而 case <-child.Done() 这个 case 又啥事不干。那这个 else 不就多余了吗?

    其实不然。我们来看 parentCancelCtx 的代码:

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

    这里只会识别三种 Context 类型:cancelCtxtimerCtx*valueCtx。若是把 Context 内嵌到一个类型里,就识别不出来了。

    由于 context 包的代码并不多,所以我直接把它 copy 出来了,然后在 else 语句里加上了几条打印语句,来验证上面的说法:

    type MyContext struct {
        // 这里的 Context 是我 copy 出来的,所以前面不用加 context.
        Context
    }
    
    func main() {
        childCancel := true
    
        parentCtx, parentFunc := WithCancel(Background())
        mctx := MyContext{parentCtx}
    
        childCtx, childFun := WithCancel(mctx)
    
        if childCancel {
            childFun()
        } else {
            parentFunc()
        }
    
        fmt.Println(parentCtx)
        fmt.Println(mctx)
        fmt.Println(childCtx)
    
        // 防止主协程退出太快,子协程来不及打印 
        time.Sleep(10 * time.Second)
    }
    

    我自已在 else 里添加的打印语句我就不贴出来了,感兴趣的可以自己动手实验下。我们看下三个 context 的打印结果:

    context.Background.WithCancel
    {context.Background.WithCancel}
    {context.Background.WithCancel}.WithCancel
    

    果然,mctx,childCtx 和正常的 parentCtx 不一样,因为它是一个自定义的结构体类型。

    else 这段代码说明,如果把 ctx 强行塞进一个结构体,并用它作为父节点,调用 WithCancel 函数构建子节点 context 的时候,Go 会新启动一个协程来监控取消信号,明显有点浪费嘛。

    再来说一下,select 语句里的两个 case 其实都不能删。

    select {
        case <-parent.Done():
            child.cancel(false, parent.Err())
        case <-child.Done():
    }
    

    第一个 case 说明当父节点取消,则取消子节点。如果去掉这个 case,那么父节点取消的信号就不能传递到子节点。

    第二个 case 是说如果子节点自己取消了,那就退出这个 select,父节点的取消信号就不用管了。如果去掉这个 case,那么很可能父节点一直不取消,这个 goroutine 就泄漏了。当然,如果父节点取消了,就会重复让子节点取消,不过,这也没什么影响嘛。

    timerCtx

    timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。

    type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.
    
        deadline time.Time
    }
    

    timerCtx 首先是一个 cancelCtx,所以它能取消。看下 cancel() 方法:

    func (c *timerCtx) cancel(removeFromParent bool, err error) {
        // 直接调用 cancelCtx 的取消方法
        c.cancelCtx.cancel(false, err)
        if removeFromParent {
            // 从父节点中删除子节点
            removeChild(c.cancelCtx.Context, c)
        }
        c.mu.Lock()
        if c.timer != nil {
            // 关掉定时器,这样,在deadline 到来时,不会再次取消
            c.timer.Stop()
            c.timer = nil
        }
        c.mu.Unlock()
    }
    

    创建 timerCtx 的方法:

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

    WithTimeout 函数直接调用了 WithDeadline,传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。也就是说,WithDeadline 需要用的是绝对时间。重点来看它:

    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
        if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
            // 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
            // 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
            // 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
            return WithCancel(parent)
        }
    
        // 构建 timerCtx
        c := &timerCtx{
            cancelCtx: newCancelCtx(parent),
            deadline:  deadline,
        }
        // 挂靠到父节点上
        propagateCancel(parent, c)
    
        // 计算当前距离 deadline 的时间
        d := time.Until(deadline)
        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 {
            // d 时间后,timer 会自动调用 cancel 函数。自动取消
            c.timer = time.AfterFunc(d, func() {
                c.cancel(true, DeadlineExceeded)
            })
        }
        return c, func() { c.cancel(true, Canceled) }
    }
    

    也就是说仍然要把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消。

    有一个特殊情况是,如果要创建的这个子节点的 deadline 比父节点要晚,也就是说如果父节点是时间到自动取消,那么一定会取消这个子节点,导致子节点的 deadline 根本不起作用,因为子节点在 deadline 到来之前就已经被父节点取消了。

    这个函数的最核心的一句是:

    c.timer = time.AfterFunc(d, func() {
        c.cancel(true, DeadlineExceeded)
    })
    

    c.timer 会在 d 时间间隔后,自动调用 cancel 函数,并且传入的错误就是 DeadlineExceeded

    var DeadlineExceeded error = deadlineExceededError{}
    
    type deadlineExceededError struct{}
    
    func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
    

    也就是超时错误。

    valueCtx

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

    由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 context。但它仍然是一个 Context,这是 Go 语言的一个特点。

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

    对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。

    通过层层传递 context,最终形成这样一棵树:
    在这里插入图片描述
    和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。

    取值的过程,实际上是一个递归查找的过程:

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

    它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。

    因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

    WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。

    如果你接手过项目,肯定经历过这样的窘境:在一个处理过程中,有若干子函数、子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。

    你根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不同的 context 节点,查找的时候,只会返回一个结果)?你肯定会崩溃的。

    而这也是 context.Value 最受争议的地方。很多人建议尽量不要通过 context 传值。

    如何使用 context

    context 使用起来非常方便。源码里对外提供了一个创建根节点 context 的函数:

    func Background() Context
    

    background 是一个空的 context, 它不能被取消,没有值,也没有超时时间。

    有了根节点 context,又提供了四个函数创建子节点 context:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    func WithValue(parent Context, key, val interface{}) Context
    

    context 会在函数传递间传递。只需要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。

    在官方博客里,对于使用 context 提出了几点建议:

    1. Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
    2. Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
    3. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
    4. The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

    我翻译一下:

    1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
    2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
    3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
    4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

    传递共享的数据

    对于 Web 服务端开发,往往希望将一个请求处理的整个过程串起来,这就非常依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 context。

    package main
    
    import (
        "context"
        "fmt"
    )
    
    func main() {
        ctx := context.Background()
        process(ctx)
    
        ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
        process(ctx)
    }
    
    func process(ctx context.Context) {
        traceId, ok := ctx.Value("traceId").(string)
        if ok {
            fmt.Printf("process over. trace_id=%s\n", traceId)
        } else {
            fmt.Printf("process over. no trace_id\n")
        }
    }
    

    运行结果:

    process over. no trace_id
    process over. trace_id=qcrao-2019
    

    第一次调用 process 函数时,ctx 是一个空的 context,自然取不出来 traceId。第二次,通过 WithValue 函数创建了一个 context,并赋上了 traceId 这个 key,自然就能取出来传入的 value 值。

    当然,现实场景中可能是从一个 HTTP 请求中获取到的 Request-ID。所以,下面这个样例可能更适合:

    const requestIDKey int = 0
    
    func WithRequestID(next http.Handler) http.Handler {
        return http.HandlerFunc(
            func(rw http.ResponseWriter, req *http.Request) {
                // 从 header 中提取 request-id
                reqID := req.Header.Get("X-Request-ID")
                // 创建 valueCtx。使用自定义的类型,不容易冲突
                ctx := context.WithValue(
                    req.Context(), requestIDKey, reqID)
    
                // 创建新的请求
                req = req.WithContext(ctx)
    
                // 调用 HTTP 处理函数
                next.ServeHTTP(rw, req)
            }
        )
    }
    
    // 获取 request-id
    func GetRequestID(ctx context.Context) string {
        ctx.Value(requestIDKey).(string)
    }
    
    func Handle(rw http.ResponseWriter, req *http.Request) {
        // 拿到 reqId,后面可以记录日志等等
        reqID := GetRequestID(req.Context())
        ...
    }
    
    func main() {
        handler := WithRequestID(http.HandlerFunc(Handle))
        http.ListenAndServe("/", handler)
    }
    
    

    取消 goroutine

    我们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,而且是每秒更新 1 次。app 端向后台发起 websocket 连接(现实中可能是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。如果用户退出此页面,则后台需要“取消”此过程,退出 goroutine,系统回收资源。

    后端可能的实现如下:

    func Perform() {
        for {
            calculatePos()
            sendResult()
            time.Sleep(time.Second)
        }
    }
    

    如果需要实现“取消”功能,并且在不了解 context 功能的前提下,可能会这样做:给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环。

    上面给出的简单做法,可以实现想要的效果,没有问题,但是并不优雅,并且一旦协程数量多了之后,并且各种嵌套,就会很麻烦。优雅的做法,自然就要用到 context。

    func Perform(ctx context.Context) {
        for {
            calculatePos()
            sendResult()
    
            select {
            case <-ctx.Done():
                // 被取消,直接返回
                return
            case <-time.After(time.Second):
                // block 1 秒钟 
            }
        }
    }
    

    主流程可能是这样的:

    ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
    go Perform(ctx)
    
    // ……
    // app 端返回页面,调用cancel 函数
    cancel()
    

    注意一个细节,WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context。

    防止 goroutine 泄漏

    前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“如果不用 context 取消,goroutine 就会泄漏的例子”,来自参考资料:【避免协程泄漏】。

    func gen() <-chan int {
        ch := make(chan int)
        go func() {
            var n int
            for {
                ch <- n
                n++
                time.Sleep(time.Second)
            }
        }()
        return ch
    }
    

    这是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏:

    func main() {
        for n := range gen() {
            fmt.Println(n)
            if n == 5 {
                break
            }
        }
        // ……
    }
    

    当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。

    用 context 改进这个例子:

    func gen(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            var n int
            for {
                select {
                case <-ctx.Done():
                    return
                case ch <- n:
                    n++
                    time.Sleep(time.Second)
                }
            }
        }()
        return ch
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
    
        for n := range gen(ctx) {
            fmt.Println(n)
            if n == 5 {
                cancel()
                break
            }
        }
        // ……
    }
    

    增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。

    context 真的这么好吗

    读完全文,你一定有这种感觉:context 就是为 server 而设计的。说什么处理一个请求,需要启动多个 goroutine 并行地去处理,并且在这些 goroutine 之间还要传递一些共享的数据等等,这些都是写一个 server 要做的事。

    没错,Go 很适合写 server,但它终归是一门通用的语言。你在用 Go 做 Leetcode 上面的题目的时候,肯定不会认为它和一般的语言有什么差别。所以,很多特性好不好,应该从 Go 只是一门普通的语言,很擅长写 server 的角度来看。

    从这个角度来看,context 并没有那么美好。Go 官方建议我们把 Context 作为函数的第一个参数,甚至连名字都准备好了。这造成一个后果:因为我们想控制所有的协程的取消动作,所以需要在几乎所有的函数里加上一个 Context 参数。很快,我们的代码里,context 将像病毒一样扩散的到处都是。

    在参考资料【Go2 应该去掉 context】这篇英文博客里,作者甚至调侃说:如果要把 Go 标准库的大部分函数都加上 context 参数的话,例如下面这样:

    n, err := r.Read(context.TODO(), p)
    

    就给我来一枪吧!

    原文是这样说的:put a bullet in my head, please.我当时看到这句话的时候,会心一笑。这可能就是陶渊明说的:每有会意,便欣然忘食。当然,我是在晚饭会看到这句话的。

    为了表达自己对 context 并没有什么好感,作者接着又说了一句:If you use ctx.Value in my (non-existent) company, you’re fired. 简直太幽默了,哈哈。

    另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 这些创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n) 复杂度的,效率不高。

    那么,context 包到底解决了什么问题呢?答案是:cancelation。仅管它并不完美,但它确实很简洁地解决了问题。

    总结

    到这里,整个 context 包的内容就全部讲完了。源码非常短,很适合学习,一定要去读一下。

    context 包是 Go 1.7 引入的标准库,主要用于在 goroutine 之间传递取消信号、超时时间、截止时间以及一些共享的值等。它并不是太完美,但几乎成了并发控制和超时控制的标准做法。

    使用上,先创建一个根节点的 context,之后根据库提供的四个函数创建相应功能的子节点 context。由于它是并发安全的,所以可以放心地传递。

    当使用 context 作为函数参数时,直接把它放在第一个参数的位置,并且命名为 ctx。另外,不要把 context 嵌套在自定义的类型里。

    最后,大家下次在看到代码里有用到 context 的,观察下是怎么使用的,肯定逃不出我们讲的几种类型。熟悉之后会发现:context 可能并不完美,但它确实简洁高效地解决了问题。

    展开全文
  • Golang 如何正确使用 Context

    千次阅读 2019-09-02 08:59:19
    How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017 视频: https://www.youtube.com/watch?v=-_B5uQ4UGi0 博文: https://medium.com/@cep21/how-to-correctly-use-context-...

    视频信息

    How to correctly use package context
    by Jack Lindamood
    at Golang UK Conf. 2017

    视频:

    https://www.youtube.com/watch?v=-_B5uQ4UGi0


    博文:

    https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39

    为什么需要 Context

    • 每一个长请求都应该有个超时限制

    • 需要在调用中传递这个超时

      • 比如开始处理请求的时候我们说是 3 秒钟超时

      • 那么在函数调用中间,这个超时还剩多少时间了?

      • 需要在什么地方存储这个信息,这样请求处理中间可以停止

    如果进一步考虑。

    640?wx_fmt=png

    如上图这样的 RPC 调用,开始调用 RPC 1 后,里面分别调用了 RPC 2, RPC 3, RPC 4,等所有 RPC 用成功后,返回结果。

    这是正常的方式,但是如果 RPC 2 调用失败了会发生什么?

    640?wx_fmt=png

    RPC 2 失败后,如果没有 Context 的存在,那么我们可能依旧会等所有的 RPC 执行完毕,但是由于 RPC 2 败了,所以其实其它的 RPC 结果意义不大了,我们依旧需要给用户返回错误。因此我们白白的浪费了 10ms,完全没必要去等待其它 RPC 执行完毕。

    那如果我们在 RPC 2 失败后,就直接给用户返回失败呢?

    640?wx_fmt=png

    用户是在 30ms 的位置收到了错误消息,可是 RPC 3 和 RPC 4 依然在没意义的运行,还在浪费计算和IO资源。

    640?wx_fmt=png

    所以理想状态应该是如上图,当 RPC 2 出错后,除了返回用户错误信息外,我们也应该有某种方式可以通知 RPC 3 和 RPC 4,让他们也停止运行,不再浪费资源。

    所以解决方案就是:

    • 用信号的方式来通知请求该停了

    • 包含一些关于什么时间请求可能会结束的提示(超时)

    • 用 channel 来通知请求结束了

    那干脆让我们把变量也扔那吧。?

    • 在 Go 中没有线程/go routine 变量

      • 其实挺合理的,因为这样就会让 goroutine 互相产生依赖

    • 非常容易被滥用

    Context 实现细节

    context.Context:

    • 是不可变的(immutable)树节点

    • Cancel 一个节点,会连带 Cancel 其所有子节点 (从上到下)

    • Context values 是一个节点

    • Value 查找是回溯树的方式 (从下到上)

    示例 Context 链

    完整代码:https://play.golang.org/p/ddpofBV1QS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package main
    func tree() {
    ctx1 := context.Background()
    ctx2, _ := context.WithCancel(ctx1)
    ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
    ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
    ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
    ctx6 := context.WithValue(ctx5, "userID", 12)
    }

    如果这样构成的 Context 链,其形如下图:

    640?wx_fmt=png

    那么当 3 秒超时到了时候:

    640?wx_fmt=png

    可以看到 ctx4 超时退出了。

    当 5秒钟 超时到达时:

    640?wx_fmt=png

    可以看到,不仅仅 ctx3 退出了,其所有子节点,比如 ctx5 和 ctx6 也都退出了。

    context.Context API

    基本上是两类操作:

    • 3个函数用于限定什么时候你的子节点退出;

    • 1个函数用于设置请求范畴的变量

      1
      2
      3
      4
      5
      6
      7
      8
      type Context interface {
      // 啥时候退出
      Deadline() (deadline time.Time, ok bool)
      Done() <-chan struct{}
      Err() error
      // 设置变量
      Value(key interface{}) interface{}
      }

    什么时候应该使用 Context?

    • 每一个 RPC 调用都应该有超时退出的能力,这是比较合理的 API 设计

    • 不仅仅 是超时,你还需要有能力去结束那些不再需要操作的行为

    • context.Context 是 Go 标准的解决方案

    • 任何函数可能被阻塞,或者需要很长时间来完成的,都应该有个 context.Context

    如何创建 Context?

    • 在 RPC 开始的时候,使用 context.Background()

      • 有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。

    • 如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO()

    • 如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子)

    如何集成到 API 里?

    • 如果有 Context,将其作为第一个变量。

      • 如 func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

      • 有些人把 context 放到中间的某个变量里去,这很不合习惯,不要那么做,放到第一个去。

    • 将其作为可选的方式,用 request 结构体方式。

      • 如:func (r *Request) WithContext(ctx context.Context) *Request

    • Context 的变量名请用 ctx(不要起一些诡异的名字?)

    Context 放哪?

    • 把 Context 想象为一条河流流过你的程序(另一个意思就是说不要喝河里的水……?)

    • 理想情况下,Context 存在于调用栈(Call Stack) 中

    • 不要把 Context 存储到一个 struct 里

      • 除非你使用的是像 http.Request 中的 request 结构体的方式

    • request 结构体应该以 Request 结束为生命终止

    • 当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference)

    • Request 结束,Context 就应该结束。(这俩是一对儿,不求同年同月同日生,但求同年同月同日死……?)

    Context 包的注意事项

    • 要养成关闭 Context 的习惯

      • 特别是 超时的 Contexts

    • 如果一个 context 被 GC 而不是 cancel 了,那一般是你做错了

      1
      2
      ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
      defer cancel()
    • 使用 Timeout 会导致内部使用 time.AfterFunc,从而会导致 context 在计时器到时之前都不会被垃圾回收。

    • 在建立之后,立即 defer cancel() 是一个好习惯。

    终止请求 (Request Cancellation)

    当你不再关心接下来获取的结果的时候,有可能会 Cancel 一个 Context?

    以 golang.org/x/sync/errgroup 为例,errgroup 使用 Context 来提供 RPC 的终止行为。

    1
    2
    3
    4
    5
    6
    type Group struct {
    cancel func()
    wg sync.WaitGroup
    errOnce sync.Once
    err error
    }

    创建一个 group 和 context:

    1
    2
    3
    4
    func WithContext(ctx context.Context) (*Group, context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    return &Group{cancel: cancel}, ctx
    }

    这样就返回了一个可以被提前 cancel 的 group。

    而调用的时候,并不是直接调用 go func(),而是调用 Go(),将函数作为参数传进去,用高阶函数的形式来调用,其内部才是 go func() 开启 goroutine。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func (g *Group) Go(f func() error) {
    g.wg.Add(1)
    go func() {
    defer g.wg.Done()
    if err := f(); err != nil {
    g.errOnce.Do(func() {
    g.err = err
    if g.cancel != nil {
    g.cancel()
    }
    })
    }
    }()
    }

    当给入函数 f 返回错误,则使用 sync.Once 来 cancel context,而错误被保存于 g.err 之中,在随后的 Wait() 函数中返回。

    1
    2
    3
    4
    5
    6
    7
    func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
    g.cancel()
    }
    return g.err
    }

    注意:这里在 Wait() 结束后,调用了一次 cancel()。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main
    func DoTwoRequestsAtOnce(ctx context.Context) error {
    eg, egCtx := errgroup.WithContext(ctx)
    var resp1, resp2 *http.Response
    f := func(loc string, respIn **http.Response) func() error {
    return func() error {
    reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
    defer cancel()
    req, _ := http.NewRequest("GET", loc, nil)
    var err error
    *respIn, err = http.DefaultClient.Do(req.WithContext(reqCtx))
    if err == nil && (*respIn).StatusCode >= 500 {
    return errors.New("unexpected!")
    }
    return err
    }
    }
    eg.Go(f("http://localhost:8080/fast_request", &resp1))
    eg.Go(f("http://localhost:8080/slow_request", &resp2))
    return eg.Wait()
    }

    在这个例子中,同时发起了两个 RPC 调用,当任何一个调用超时或者出错后,会终止另一个 RPC 调用。这里就是利用前面讲到的 errgroup 来实现的,应对有很多并非请求,并需要集中处理超时、出错终止其它并发任务的时候,这个 pattern 使用起来很方便。

    Context.Value - Request 范畴的值

    context.Value API 的万金油(duct tape)

    胶带(duct tape) 几乎可以修任何东西,从破箱子,到人的伤口,到汽车引擎,甚至到NASA登月任务中的阿波罗13号飞船(Yeah! True Story)。所以在西方文化里,胶带是个“万能”的东西。在中文里,恐怕万金油是更合适的对应词汇,从头疼、脑热,感冒发烧,到跌打损伤几乎无所不治。

    当然,治标不治本,这点东西方文化中的潜台词都是一样的。这里提及的 context.Value 对于 API 而言,就是这类性质的东西,啥都可以干,但是治标不治本。

    • value 节点是 Context 链中的一个节点

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      package context
      type valueCtx struct {
      Context
      key, val interface{}
      }
      func WithValue(parent Context, key, val interface{}) Context {
      // ...
      return &valueCtx{parent, key, val}
      }
      func (c *valueCtx) Value(key interface{}) interface{} {
      if c.key == key {
      return c.val
      }
      return c.Context.Value(key)
      }

    可以看到,WithValue() 实际上就是在 Context 树形结构中,增加一个节点罢了。

    Context 是 immutable 的。

    约束 key 的空间

    为了防止树形结构中出现重复的键,建议约束键的空间。比如使用私有类型,然后用 GetXxx() 和 WithXxxx() 来操作私有实体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type privateCtxType string
    var (
    reqID = privateCtxType("req-id")
    )
    func GetRequestID(ctx context.Context) (int, bool) {
    id, exists := ctx.Value(reqID).(int)
    return id, exists
    }
    func WithRequestID(ctx context.Context, reqid int) context.Context {
    return context.WithValue(ctx, reqID, reqid)
    }

    这里使用 WithXxx 而不是 SetXxx 也是因为 Context 实际上是 immutable 的,所以不是修改 Context 里某个值,而是产生新的 Context 带某个值。

    Context.Value 是 immutable 的

    再多次的强调 Context.Value 是 immutable 的也不过分。

    • context.Context 从设计上就是按照 immutable (不可变的)模式设计的

    • 同样,Context.Value 也是 immutable 的

    • 不要试图在 Context.Value 里存某个可变更的值,然后改变,期望别的 Context 可以看到这个改变

      • 更别指望着在 Context.Value 里存可变的值,最后多个 goroutine 并发访问没竞争冒险啥的,因为自始至终,就是按照不可变来设计的

      • 比如设置了超时,就别以为可以改变这个设置的超时值

    • 在使用 Context.Value 的时候,一定要记住这一点

    应该把什么放到 Context.Value 里?

    • 应该保存 Request 范畴的值

      • 任何关于 Context 自身的都是 Request 范畴的(这俩同生共死)

      • 从 Request 数据衍生出来,并且随着 Request 的结束而终结

    什么东西不属于 Request 范畴?

    • 在 Request 以外建立的,并且不随着 Request 改变而变化

      • 比如你 func main() 里建立的东西显然不属于 Request 范畴

    • 数据库连接

      • 如果 User ID 在连接里呢?(稍后会提及)

    • 全局 logger

      • 如果 logger 里需要有 User ID 呢?(稍后会提及)

    那么用 Context.Value 有什么问题?

    • 不幸的是,好像所有东西都是由请求衍生出来的

    • 那么我们为什么还需要函数参数?然后干脆只来一个 Context 就完了?

      1
      2
      3
      func Add(ctx context.Context) int {
      return ctx.Value("first").(int) + ctx.Value("second").(int)
      }

    曾经看到过一个 API,就是这种形式:

    1
    2
    3
    4
    func IsAdminUser(ctx context.Context) bool {
    userID := GetUser(ctx)
    return authSingleton.IsAdmin(userID)
    }

    这里API实现内部从 context 中取得 UserID,然后再进行权限判断。但是从函数签名看,则完全无法理解这个函数具体需要什么、以及做什么。

    代码要以可读性为优先设计考虑。

    别人拿到一个代码,一般不是掉进函数实现细节里去一行行的读代码,而是会先浏览一下函数接口。所以清晰的函数接口设计,会更加利于别人(或者是几个月后的你自己)理解这段代码。

    一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。如果我们将上面的接口改为:

    1
    func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool

    我们从这个函数签名就可以清楚的知道:

    • 这个函数很可能可以提前被 cancel

    • 这个函数需要 User ID

    • 这个函数需要一个authenticator来

    • 而且由于 authenticator 是传入参数,而不是依赖于隐式的某个东西,我们知道,测试的时候就很容易传入一个模拟认证函数来做测试

    • userID 是传入值,因此我们可以修改它,不用担心影响别的东西

    所有这些信息,都是从函数签名得到的,而无需打开函数实现一行行去看。

    那什么可以放到 Context.Value 里去?

    现在知道 Context.Value 会让接口定义更加模糊,似乎不应该使用。那么又回到了原来的问题,到底什么可以放到 Context.Value 里去?换个角度去想,什么不是衍生于 Request?

    • Context.Value 应该是告知性质的东西,而不是控制性质的东西

    • 应该永远都不需要写进文档作为必须存在的输入数据

    • 如果你发现你的函数在某些 Context.Value 下无法正确工作,那就说明这个 Context.Value 里的信息不应该放在里面,而应该放在接口上。因为已经让接口太模糊了。

    什么东西不是控制性质的东西?

    • Request ID

      • 而 logger 本身不是 Request 范畴,所以 logger 不应该在 Context 里

      • 非 Request 范畴的 logger 应该只是利用 Context 信息来修饰日志

      • 只是给每个 RPC 调用一个 ID,而没有实际意义

      • 这就是个数字/字符串,反正你也不会用其作为逻辑判断

      • 一般也就是日志的时候需要记录一下

    • User ID (如果仅仅是作为日志用)

    • Incoming Request ID

    什么显然是控制性质的东西?

    • 数据库连接

      • 显然会非常严重的影响逻辑

      • 因此这应该在函数参数里,明确表示出来

    • 认证服务(Authentication)

      • 显然不同的认证服务导致的逻辑不同

      • 也应该放到函数参数里,明确表示出来

    例子

    调试性质的 Context.Value - net/http/httptrace

    https://medium.com/@cep21/go-1-7-httptrace-and-context-debug-patterns-608ae887224a

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package main
    func trace(req *http.Request, c *http.Client) {
    trace := &httptrace.ClientTrace{
    GotConn: func(connInfo httptrace.GotConnInfo) {
    fmt.Println("Got Conn")
    },
    ConnectStart: func(network, addr string) {
    fmt.Println("Dial Start")
    },
    ConnectDone: func(network, addr string, err error) {
    fmt.Println("Dial done")
    },
    }
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
    c.Do(req)
    }

    net/http 是怎么使用 httptrace 的?

    • 如果有 trace 存在的话,就执行 trace 回调函数

    • 这只是告知性质,而不是控制性质

      • http 不会因为存在 trace 与否就有不同的执行逻辑

      • 这里只是告知 API 的用户,帮助用户记录日志或者调试

      • 因此这里的 trace 是存在于 Context 里的

        1
        2
        3
        4
        5
        6
        7
        8
        9
        package http
        func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
        // ...
        trace := httptrace.ContextClientTrace(req.Context())
        // ...
        if trace != nil && trace.WroteHeaders != nil {
        trace.WroteHeaders()
        }
        }

    回避依赖注入 - github.com/golang/oauth2

    • 这里比较诡异,使用 ctx.Value 来定位依赖

    • 不推荐这样做

      • 这里这样做基本上只是为了满足测试需求

        1
        2
        3
        4
        5
        6
        7
        8
        package main
        import "github.com/golang/oauth2"
        func oauth() {
        c := &http.Client{Transport: &mockTransport{}}
        ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
        conf := &oauth2.Config{ /* ... */ }
        conf.Exchange(ctx, "code")
        }

    人们滥用 Context.Value 的原因

    • 中间件的抽象

    • 很深的函数调用栈

    • 混乱的设计

    context.Value 并没有让你的 API 更简洁,那是假象,相反,它让你的 API 定义更加模糊。

    总结 Context.Value

    • 对于调试非常方便

    • 将必须的信息放入 Context.Value 中,会让接口定义更加不透明

    • 如果可以尽量明确定义在接口

    • 尽量不要用 Context.Value

    总结 Context

    • 所有的长的、阻塞的操作都需要 Context

    • errgroup 是构架于 Context 之上很好的抽象

    • 当 Request 的结束的时候,Cancel Context

    • Context.Value 应该被用于告知性质的事物,而不是控制性质的事物

    • 约束 Context.Value 的键空间

    • Context 以及 Context.Value 应该是不可变的(immutable),并且应该是线程安全

    • Context 应该随 Request 消亡而消亡

    Q&A

    数据库的访问也用 Context 么?

    之前说过长时间、可阻塞的操作都用 Context,数据库操作也是如此。不过对于超时 Cancel 操作来说,一般不会对写操作进行 cancel;但是对于读操作,一般会有 Cancel 操作。

    原文

    https://blog.lab99.org/post/golang-2017-10-27-video-how-to-correctly-use-package-context.html

    < END >

    喜欢就点个在看 or 转发个朋友圈呗

                640?wx_fmt=jpeg

    衣舞晨风


    展开全文
  • ...to <a href="https://golang.org/pkg/context/#Context" rel="nofollow noreferrer"><code>context.Context</code></a> in Go? What should be used in building a Go microservice? </div>
  • Context Context 被翻译成上下文,是应用程序环境的全局信息接口,它是一个抽象类,其实现由 Android 系统提供的 ContextImpl 类。我们通过 Context 可以非常方便的访问应用程序的资源和类,以及可以进行应用程序级...
  • Golang 之context用法

    千次阅读 2020-06-24 18:33:26
    context.go2.0 结构图2.1 Context interface2.2 emptyCtx2.3 cancelCtx2.4 valueCtx2.5 timerCtx3. 使用示例3.1 WithCancel3.2 WithDeadline3.3 WithTimeout3.4 WithValue 1. context Golang 中的context 是Go语言...
  • spring-context-support.jar

    千次下载 热门讨论 2013-07-17 17:57:45
    spring-context-support.jar spring整合任务调度quartz必需jar 还需要quartz-all-x.x.x.jar quartz版本必需在1.8.x以下
  • 揭秘Context(上下文)

    万次阅读 多人点赞 2019-03-08 00:03:13
    本文主要记述,Context到底是什么、如何理解Context、一个APP可以有几个ContextContext能干啥、Context的作用域、获取Context、全局获取Context技巧。 思考: Java:万物皆对象。Flutter:万物皆组件。 俗语:”没...
  • React Context用法整理(附完整代码)

    千次阅读 2020-11-18 16:53:14
    文章目录前言知识点汇总场景1:使用`Provider`和`Consumer`生产和消费数据文件目录及说明代码文件效果场景2:使用`ContextType`接收数据文件目录及说明代码文件效果场景3:动态和静态Context代码文件效果场景4:在...
  • 理解Go的Context机制

    千次阅读 2019-01-21 15:21:25
    1. 什么是Context 最近在分析gRPC源码,proto文件生成部分的代码,接口函数的第一个参数统一是ctx context.Context,对这种设计甚是迷惑,于是找些资料,对其背后的原理一探究竟。 Context通常被译作上下文,它是...
  • Context是什么

    万次阅读 多人点赞 2018-03-12 22:40:17
    1.Context是什么 相信很多人多会问Context到底是什么? 我们可以理解为“上下文”:它贯穿整个应用; 也可以理解成“运行环境”:它提供了一个应用运行所需要的信息,资源,系统服务等; 同样可以理解成“场景”...
  • React 中 ContextcontextType的使用

    千次阅读 2019-06-24 19:31:38
    React 中 Context 的使用 context Context 提供了一种方式,能够让数据在组件树中传递时不必一级一级的手动传递 一般情况下,数据在组件中,要一级一级的传递,单向数据流,比如Parent组件中的theme值,需要在Item...
  • go contextcontext.Background()

    千次阅读 2021-06-01 10:18:54
    context.Background() context.Background() 返回一个空的Context 我们可以用这个 空的 Context 作为 goroutine 的root 节点(如果把整个 goroutine 的关系看作 树状) 使用context.WithCancel(parent)函数,创建一...
  • Golang 中 context(上下文)使用

    千次阅读 2020-05-09 14:38:24
    1.为什么需要context 2.context包简介 3. 场景举例—等待组 4. 场景举例—通道+select 5. 场景举例—普通context 6. 场景举例—Context超时 7. 场景举例—Context传递元数据 8. context总结 1.为什么需要...
  • spring-context.jar jar包 上下文

    千次下载 热门讨论 2012-05-26 22:41:45
    jar包分享,你懂的 spring-context.jar spring-context.jar spring-context.jar spring-context.jar spring-context.jar
  • k8s集群namespace和context使用

    千次阅读 2020-09-04 18:29:29
    Available Commands: current-context Displays the current-context delete-cluster Delete the specified cluster from the kubeconfig delete-context Delete the specified context from the kubeconfig get-...
  • 从源码分析 Application、Activity、Service 中 Context 的关系和区别 我们来简单回顾一下 Context 是什么? Context 被翻译成上下文,是应用程序环境的全局信息接口,它是一个抽象类,其实现由 Android 系统提供的...
  • kotlin中withContext和async区别

    千次阅读 2021-12-03 19:28:17
    withContext 1.用于执行耗时任务,会切换线程 2.多个withContext()执行,为串行,运行在同一个线程 3.直接返回结果 4.withConext是个 suspend 函数 val task1 = withContext(Dispatchers.IO) {...} val task2 = ...
  • 由浅入深聊聊Golang的context

    万次阅读 多人点赞 2018-10-07 01:49:13
    你可以认为所有的Context是树的结构,Background是树的根,当任一Context被取消的时候,那么继承它的Context 都将被回收。 2.context实战应用 2.1 WithCancel 实现源码: func WithCancel(parent Context) ...
  • go context之WithCancel的使用

    万次阅读 多人点赞 2020-07-11 21:03:25
    实验如下图 ... ctx, cancel := context.WithCancel(context.Background()) // 父context的子协程 go watch1(ctx) // 子context,注意:这里虽然也返回了cancel的函数对象,但是未使用 valueCtx, _
  • gin-巧用Context传递多种参数

    千次阅读 2021-04-29 15:30:39
    gin有自带的Context即gin.Context,gin.Context中包含了web端发送来的http.Request、url等众多信息,但是在使用的过程中,这些信息往往是不够用的,因此,本文介绍了在gin路由中如何搭配gin.Context来进行多种参数的...
  • 广播onReceive()方法的context类型探究

    万次阅读 2018-07-18 21:09:01
    非Activity类型的context并没有所谓的任务栈; 2). 出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而...
  • Kotlin协程之withContext

    千次阅读 2021-05-27 14:45:54
    withContext必须在协程或者suspend函数中调用,否则会报错。它必须显示指定代码块所运行的线程,它会阻塞当前上下文线程,有返回值,会返回代码块的最后一行的值。 1. 指定代码块所运行的线程 它和launch一样,通过...
  • react中的context,provider使用步骤

    千次阅读 2020-11-18 16:22:01
    Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 具体详见:context官方API 本篇文章只是详细的把我个人的学习日志给记录下来相当于又会了一个react相关的知识点,具体这些...
  • Context完全解析(一)什么是Context?

    万次阅读 2018-02-11 15:01:57
    Context字面意思是上下文环境,它是一个抽象类,定义很多访问应用程序环境中全局信息的接口,其正在的实现是在ContextImpl类中。通过它可以访问应用程序的资源和相关的类,比如:Resources,AssetManager,Package,...
  • build context to Docker daemon 3.314GB 意思是:正在将生成上下文发送到Docker守护程序。如果dockerfile的同级目录文件过多过大,docker build的时候会向上下文环境发送,导致很慢,这个时候的解决办法有: 1....
  • go - context 用法

    千次阅读 2017-11-13 13:41:46
    1,context作用1,通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。形象地说,假如一个请求过来,需要A去做事情,而A让B...
  • ViewModel中传入Context的方法

    千次阅读 2020-12-24 16:29:41
    ViewModel使用的越来越多了,严格来说,官方并不建议你在ViewModel中添加Context的引用。同时,ViewModel的构造方法是没有任何参数的,有的时候会很不灵活。以下记录两种方法。 #1.通过kotlin的拓展函数 fun <T ...
  • go context用法详解

    千次阅读 2018-07-20 17:59:28
    转发自 作者kingeasternsun ... 本文主要基于官方文档Go Concurrency Patterns: Context以及视频Advanced Go Concurrency Patterns的学习而得。 背景 在go服务器中,对于每个请求的request都是在单独的goroutine...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,569,745
精华内容 1,027,898
关键字:

context