golang之context详解

Python016

golang之context详解,第1张

为什么需要context

在go服务器中,对于每个请求的request都是在单独的goroutine中进行的,处理一个request也可能设计多个goroutine之间的交互, 使用context可以使开发者方便的在这些goroutine里传递request相关的数据、取消goroutine的signal或截止日期

在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。熟悉channel的朋友应该都见过使用done channel来处理此类问题。比如以下这个例子:

上述例子中定义了一个buffer为0的channel done, 子协程运行着定时任务如果主协程需要在某个时刻发送消息通知子协程中断任务退出,那么就可以让子协程监听这个done channel,一旦主协程关闭done channel,那么子协程就可以推出了,这样就实现了主协程通知子协程的需求。这很好,但是这也是有限的。

如果我们可以在简单的通知上附加传递额外的信息来控制取消:为什么取消,或者有一个它必须要完成的最终期限,更或者有多个取消选项,我们需要根据额外的信息来判断选择执行哪个取消选项。

考虑下面这种情况:假如主协程中有多个任务1, 2, …m,主协程对这些任务有超时控制;而其中任务1又有多个子任务1, 2, …n,任务1对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号。

如果还是使用done channel的用法,我们需要定义两个done channel,子任务们需要同时监听这两个done channel。嗯,这样其实好像也还行哈。但是如果层级更深,如果这些子任务还有子任务,那么使用done channel的方式将会变得非常繁琐且混乱。

我们需要一种优雅的方案来实现这样一种机制:

上层任务取消后,所有的下层任务都会被取消;中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。

这个时候context就派上用场了。我们首先看看context的结构设计和实现原理。

context接口

先看Context接口结构,看起来非常简单。

}

Context接口包含四个方法:

Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。

Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。

Err 如果Done返回的channel没有关闭,将返回nil如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。

Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil。

可以看到Done方法返回的channel正是用来传递结束信号以抢占并中断当前任务;Deadline方法指示一段时间后当前goroutine是否会被取消;以及一个Err方法,来解释goroutine被取消的原因;而Value则用于获取特定于当前任务树的额外信息。而context所包含的额外信息键值对是如何存储的呢?其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到key所对应的值,就会向上去父节点里找,直到根节点。

emptyCtx

emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。

Background和TODO只是用于不同场景下: Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background;而TODO是在不确定使用什么context的时候才会使用。

用法

下列程序,已经经过实验,可以满足题目要求.

ORG 0000H

SJMP MAIN

ORG 000BH

SJMP T0_INT

MAIN:

MOV TMOD, #01H T0定时方式1

MOV TH0, #(65536-50000) / 256 定时50ms@12MHz

MOV TL0, #(65536-50000) MOD 256

SETB TR0 启动T0

MOV IE, #82H 开中断

第一秒钟L0,L2亮,第二秒钟L1,L3亮,第三秒L4,L6亮,第四秒钟L5,L7亮,

第五秒L0,L2,L4,L6亮,第六秒钟,L1,L3,L5,L7亮,第七秒钟八个二极管全亮,第八秒钟全灭.

MOV 30H, #11111010B

MOV 31H, #11110101B

MOV 32H, #10101111B

MOV 33H, #01011111B

MOV 34H, #10101010B

MOV 35H, #01010101B

MOV 36H, #00000000B

MOV 37H, #11111111B

MOV R0, #30H

MOV R7, #20

M_LOOP:

SJMP M_LOOP无限循环

T0_INT:

MOV TL0, #(65536-50000) MOD 256

MOV TH0, #(65536-50000) / 256 定时50ms@12MHz

DJNZ R7, T0_END

MOV R7, #20

MOV P1, @R0

INC R0

CJNE R0, #38H, T0_END

MOV R0, #30H

T0_END:

RETI

END完

在linux下实现定时器主要有如下方式

在这当中 基于时间轮方式实现的定时器 时间复杂度最小,效率最高,然而我们可以通过 优先队列 实现时间轮定时器。

优先队列的实现可以使用最大堆和最小堆,因此在队列中所有的数据都可以定义排序规则自动排序。我们直接通过队列中 pop 函数获取数据,就是我们按照自定义排序规则想要的数据。

在 Golang 中实现一个优先队列异常简单,在 container/head 包中已经帮我们封装了,实现的细节,我们只需要实现特定的接口就可以。

下面是官方提供的例子

因为优先队列底层数据结构是由二叉树构建的,所以我们可以通过数组来保存二叉树上的每一个节点。

改数组需要实现 Go 预先定义的接口 Len , Less , Swap , Push , Pop 和 update 。

timerType结构是定时任务抽象结构

首先的 start 函数,当创建一个 TimeingWheel 时,通过一个 goroutine 来执行 start ,在start中for循环和select来监控不同的channel的状态

通过for循环从队列中取数据,直到该队列为空或者是遇见第一个当前时间比任务开始时间大的任务, append 到 expired 中。因为优先队列中是根据 expiration 来排序的,

所以当取到第一个定时任务未到的任务时,表示该定时任务以后的任务都未到时间。

当 getExpired 函数取出队列中要执行的任务时,当有的定时任务需要不断执行,所以就需要判断是否该定时任务需要重新放回优先队列中。 isRepeat 是通过判断任务中 interval 是否大于 0 判断,

如果大于0 则,表示永久就生效。

防止外部滥用,阻塞定时器协程,框架又一次封装了timer这个包,名为 timer_wapper 这个包,它提供了两种调用方式。

参数和上面的参数一样,只是在第三个参数中使用了任务池,将定时任务放入了任务池中。定时任务的本身执行就是一个 put 操作。

至于put以后,那就是 workers 这个包管理的了。在 worker 包中, 也就是维护了一个任务池,任务池中的任务会有序的执行,方便管理。