首页 > 其他分享 >深度解密 go Context

深度解密 go Context

时间:2023-09-05 14:23:50浏览次数:33  
标签:Context 解密 接口 Done context go 超时 channel

问题: 不处理Done 为怎么样,会泄漏吗?

问题:Done() 是如何实现,全部子context接受的?

问题:没有及时处理context 的Done 消息,会怎么样,后续处理能来的及吗?

 

Go 语言的 context 包短小精悍,非常适合新手学习。不论是它的源码还是实际使用,都值得投入时间去学习。

这篇文章依然想尝试全面、深入地去研究。文章相比往期而言,整体不长,希望你看完可以有所收获!

什么是 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 来使用。一旦关闭,就会立即读出零值。

 

 

 

参考:

https://zhuanlan.zhihu.com/p/68792989

标签:Context,解密,接口,Done,context,go,超时,channel
From: https://www.cnblogs.com/rebrobot/p/17679428.html

相关文章

  • django-celery定时任务(beat)
    前言Celery可以异步执行,也可以通过定时任务触发Django中使用Celery要在Django项目中使用Celery,您必须首先定义Celery库的一个实例(称为“应用程序”)如果你有一个现代的Django项目布局,比如: 创建一个celery模块,来定义celery实例importo......
  • 生产环境部署Django项目
    生产环境部署Django项目1、 部署架构IP地址安装服务  172.16.1.251nginxuwsgi(sock方式)dockermysql5.7redis5 Nginx前端Web服务,接收到动态请求通过uwsgi模块将请求转发给uwsgi服务器,uwsgi服务器通过django处理完后返回给Nginx,Nginx返回用户浏览器展示。 既然uwsgi是一个......
  • Django中创建项目步骤
    step1:创建一个文件夹projectstep2:进入cmd,进入该文件夹cdE:/pythoncode/projectstep3:创建Django项目,该项目名称为“project_name”django-adminstartprojectproject_name创建成功......
  • 4.go语言复合类型简述
    目录1.本章前瞻2.来自leetcode的例题描述分析题解3.复合类型新版本的变化3.1string和[]byte的高效转化3.2内置函数clear4.复合类型概述4.1切片4.2映射4.3字符串4.3.1字符串的底层机构4.3.2string和[]byte的转化6.下一篇1.本章前瞻很好,经过很长的时间,你终于来到go语......
  • go channel 内部结构
            参考:https://blog.csdn.net/QiuHaoqian/article/details/108999754......
  • 软件测试|Django 入门:构建Python Web应用的全面指南
    引言Django是一个强大的PythonWeb框架,它以快速开发和高度可扩展性而闻名。本文将带您深入了解Django的基本概念和核心功能,帮助您从零开始构建一个简单的Web应用。什么是Django?Django是一个基于MVC(模型-视图-控制器)设计模式的Web框架,旨在简化Web应用程序的开发过程。它由Django软......
  • 01 - 安装 Rust 和使用 Cargo
    一、Rust介绍Rust是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。Rust最初是由Mozilla研究院的GraydonHoare设计创造,然后在DaveHerman,Br......
  • 2023-09-05:请用go语言编写。一个图像有n个像素点,存储在一个长度为n的数组arr里, 每个像
    2023-09-05:请用go语言编写。一个图像有n个像素点,存储在一个长度为n的数组arr里,每个像素点的取值范围[0,s]的整数,请你给图像每个像素点值加上一个整数k(可以是负数),像素值会自动截取到[0,s]范围,当像素值<0,会更改为0,当新像素值>s,会更改为s,这样就可以得到新的arr,想让所有像素点的......
  • Go语言实现Kafka消费者的示例代码
    Kafka是一种分布式流处理平台,由Facebook于2011年推出,现在已经成为Apache项目的一部分。Kafka提供了高可用性、可扩展性和低延迟的消息传递服务,适用于处理实时和离线数据。Kafka的主要功能包括生产者-消费者通信、批处理和实时数据流处理。Kafka基于发布/订阅模型,允许消息发布者将数......
  • GORM实践
    GORM(GoObjectRelationalMapping)是一个在Go语言中使用的对象关系映射库,它提供了一种简洁、灵活的方式来操作数据库。GORM支持多种关系型数据库,如MySQL、PostgreSQL、SQLite和SQLServer等。GORM的特点包括:简单易用:GORM提供了简洁直观的API,使得数据库操作变得简单易用。ORM功能:GOR......