1. context 常用方法,以及各种适用于什么场景
1.1 context含有的方法
var ctx context. Context
var cancel context. CancelFunc
ctx = context. WithValue ( context. Background ( ) , "key" , "value" )
ctx, cancel = context. WithTimeout ( context. Background ( ) , time. Second* 10 )
ctx, cancel = context. WithDeadline ( context. Background ( ) , time. Now ( ) . Add ( time. Second* 10 ) )
ctx, cancel = context. WithCancel ( context. Background ( ) )
defer cancel ( )
1.2 方法适用场景和伪代码示例
1.2.1 值传递:比如gin框架中用来传递key,value的值,自己简单示例如下
func readContext ( ctx context. Context) {
traceId, ok := ctx. Value ( "key" ) . ( string )
if ok {
fmt. Printf ( "readContext key=%s\n" , traceId)
} else {
fmt. Printf ( "readContext no key\n" )
}
}
func main ( ) {
ctx := context. Background ( )
readContext ( ctx)
ctx = context. WithValue ( ctx, "key" , "beautiful" )
readContext ( ctx)
}
func TestWithValueContext ( t * testing. T) {
main ( )
}
1.2.2 超时控制-timeout: http请求设置超时时间
func httpRequest ( ctx context. Context) {
for {
select {
case <- ctx. Done ( ) :
fmt. Println ( "http requests cancel" )
return
case <- time. After ( time. Second * 1 ) :
}
}
}
func TestTimeoutContext ( t * testing. T) {
fmt. Println ( "start TestTimeoutContext" )
ctx, cancel := context. WithTimeout ( context. Background ( ) , time. Second* 2 )
defer cancel ( )
httpRequest ( ctx)
time. Sleep ( time. Second * 5 )
}
1.2.3, 超时控制-deadline: 比如文件io或者网络io等耗时操作,可以查看剩余的时间是否充足,决定是否进行下一步操作
func copyFile ( ctx context. Context) {
deadline, ok := ctx. Deadline ( )
if ok == false {
return
}
isEnough := deadline. Sub ( time. Now ( ) ) > time. Second* 5
if isEnough {
fmt. Println ( "copy file" )
} else {
fmt. Println ( "isEnough is false return" )
return
}
}
func TestDeadlineContext ( t * testing. T) {
ctx, cancel := context. WithDeadline ( context. Background ( ) , time. Now ( ) . Add ( time. Second* 4 ) )
defer cancel ( )
copyFile ( ctx)
time. Sleep ( time. Second * 5 )
}
1.2.4. 取消控制: goroutine发送取消信号,保证自己这个逻辑中发散出去的goroutine全部成功取消
func gen ( ctx context. Context) <- chan int {
ch := make ( chan int )
go func ( ) {
var n int
for {
select {
case ch <- n:
n++
time. Sleep ( time. Second)
case <- ctx. Done ( ) :
return
}
}
} ( )
return ch
}
func TestCancelContext ( t * testing. T) {
ctx, cancel := context. WithCancel ( context. Background ( ) )
defer cancel ( )
for n := range gen ( ctx) {
fmt. Println ( n)
if n == 5 {
cancel ( )
break
}
}
}
2. context包的底层实现是什么样的?
2.1 key,value 传值底层实现
函数底层实现代码(golang v1.16),其核心就是当本context无法获取到key的值的时候,递归父context获取
type valueCtx struct {
Context
key, val interface { }
}
func WithValue ( parent Context, key, val interface { } ) Context {
if parent == nil {
panic ( "cannot create context from nil parent" )
}
if key == nil {
panic ( "nil key" )
}
if ! reflectlite. TypeOf ( key) . Comparable ( ) {
panic ( "key is not comparable" )
}
return & valueCtx{ parent, key, val}
}
func ( c * valueCtx) Value ( key interface { } ) interface { } {
if c. key == key {
return c. val
}
return c. Context. Value ( key)
}
2.2 cancel实现
2.2.1 cancelCtx 结构体
type cancelCtx struct {
Context
mu sync. Mutex
done chan struct { }
children map [ canceler] struct { }
err error
}
2.2.2 cancelCtx实现了cancel函数,逻辑如下
1, 锁保证并发冲突,避免并发冲突 2,关闭c.done这个channel,通过这个传递信号(往后细化分析) 3,遍历关闭所有子节点,保证不会内存泄漏 4,释放自己的所有子节点后,将自己的子节点map赋值为nil 5,将自己从自己的父节点中进行移除,这个只有在调用WithCancel()方法的时候会触发,也就是说传入参数removeFromParent为true(往后细化分析)
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
}
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)
}
}
2.2.3 细化:c.done的信号传递
这个是基于所有channel的特性,当监听一个channel,channel为空的时候会阻塞,但是如果channel被关闭,那么将不会阻塞,而会读取到一个空值 基于上述特性,实现了关闭这个channel,而其他所有监听此channel的goroutine都收到此信号 代码举例
func Done ( ch chan struct { } , count int ) {
for {
select {
case <- ch:
fmt. Println ( count)
return
}
}
}
func TestCloseChannel ( t * testing. T) {
signalChannel := make ( chan struct { } )
go Done ( signalChannel, 1 )
go Done ( signalChannel, 2 )
go Done ( signalChannel, 3 )
time. Sleep ( 3 )
fmt. Println ( "close signalChannel" )
close ( signalChannel)
select {
}
}
2.2.3 细化:removeFromParent参数-是否从父节点delete自己
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 ( )
}
为什么调用WithCancel()的时候,也就是新建一个节点的时候会传入removeFromParent=true然后调用removeChild()呢?
因为你调用cancel作用的更多的处理的挂靠在你这个context上的子节点,而只有最后一步才是真正的释放自己 举例:
1,第一步:假如你创建的一个cancelContext,挂靠在在根节点上(contextBackgroud)上,那你下面的子节点都会因为你的 c.children = nil 而释放。 2,第二步:然后逻辑上你自己都调用了cancel,那么你自己也要释放了,所以就将自己从从父节点中delete的 为什么其他删除子节点的时候不会调用?
1,因为其中有一个操作是 delete(p.children, child) ,这个操作会删除父节点的子节点的map中的自己,而一边遍历和一边删除map是会出问题的 2,同时由于cancel()函数中有操作为 c.children = nil ,所以也无需说去做这种操作