go语言协程线程安全 golang线程安全( 二 )


valueCtx利用一个Context类型的变量来表示父节点context,所以当前context继承了父context的所有信息;valueCtx类型还携带一组键值对,也就是说这种context可以携带额外的信息 。valueCtx实现了Value方法,用以在context链路上获取key对应的值,如果当前context上不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点 。
WithValue用以向context添加键值对:
【go语言协程线程安全 golang线程安全】这里添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链 。获取value的过程就是在这条context链上由尾部上前搜寻:
跟valueCtx类似 , cancelCtx中也有一个context变量作为父节点;变量done表示一个channel,用来表示传递关闭信号;children表示一个map,存储了当前context节点下的子节点;err用于存储错误信息表示任务结束的原因 。
再来看一下cancelCtx实现的方法:
可以发现cancelCtx类型变量其实也是canceler类型,因为cancelCtx实现了canceler接口 。Done方法和Err方法没必要说了,cancelCtx类型的context在调用cancel方法时会设置取消原因,将done channel设置为一个关闭channel或者关闭channel,然后将子节点context依次取消 , 如果有需要还会将当前节点从父节点上移除 。
WithCancel函数用来创建一个可取消的context,即cancelCtx类型的context。WithCancel返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作 。直接看源码:
之前说到cancelCtx取消时 , 会将后代节点中所有的cancelCtx都取消,propagateCancel即用来建立当前节点与祖先节点这个取消关联逻辑 。
这里或许有个疑问,为什么是祖先节点而不是父节点?这是因为当前context链可能是这样的:
当前cancelCtx的父节点context并不是一个可取消的context,也就没法记录children。
timerCtx是一种基于cancelCtx的context类型 , 从字面上就能看出,这是一种可以定时取消的context。
timerCtx内部使用cancelCtx实现取消,另外使用定时器timer和过期时间deadline实现定时取消的功能 。timerCtx在调用cancel方法,会先将内部的cancelCtx取消,如果需要则将自己从cancelCtx祖先节点上移除,最后取消计时器 。
WithDeadline返回一个基于parent的可取消的context,并且其过期时间deadline不晚于所设置时间d。
与WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点 , 而WithTimeout接收一个相对当前时间的过期时长timeout:
首先使用context实现文章开头done channel的例子来示范一下如何更优雅实现协程间取消信号的同步:
这个例子中 , 只要让子线程监听主线程传入的ctx,一旦ctx.Done()返回空channel,子线程即可取消执行任务 。但这个例子还无法展现context的传递取消信息的强大优势 。
阅读过net/http包源码的朋友可能注意到在实现http server时就用到了context, 下面简单分析一下 。
1、首先Server在开启服务时会创建一个valueCtx,存储了server的相关信息 , 之后每建立一条连接就会开启一个协程,并携带此valueCtx。
2、建立连接之后会基于传入的context创建一个valueCtx用于存储本地地址信息 , 之后在此基础上又创建了一个cancelCtx,然后开始从当前连接中读取网络请求,每当读取到一个请求则会将该cancelCtx传入,用以传递取消信号 。一旦连接断开 , 即可发送取消信号,取消所有进行中的网络请求 。
3、读取到请求之后,会再次基于传入的context创建新的cancelCtx,并设置到当前请求对象req上,同时生成的response对象中cancelCtx保存了当前context取消方法 。

推荐阅读