32 | context.Context类型
该思维导图由 AI 生成,仅供参考
前导内容:WaitGroup 值补充知识
- 深入了解
- 翻译
- 解释
- 总结
Go语言中的`context.Context`类型是一个强大的同步工具,用于实现一对多的goroutine协作流程。该类型可以被传播给多个goroutine,并携带额外的信息和信号。文章介绍了如何使用`context`包中的函数和`Context`类型来实现一对多的goroutine协作流程。通过调用`context.Background`函数和`context.WithCancel`函数,可以得到一个可撤销的`Context`值和一个撤销函数。此外,还介绍了`WithDeadline`、`WithTimeout`和`WithValue`等函数,它们可以用来产生不同类型的`Context`值。文章还提到了`Context`值可以被繁衍,形成一颗代表上下文全貌的树形结构。总的来说,`context.Context`类型为Go语言中的并发编程提供了更加灵活和强大的工具,能够更好地管理goroutine之间的协作流程。文章还深入讨论了撤销信号的传播方式和含数据的`Context`值的使用方法,为读者提供了全面的技术视角。
《Go 语言核心 36 讲》,新⼈⾸单¥59
全部留言(46)
- 最新
- 精选
- 拂尘@郝老师 有几点疑问烦劳回答下,谢谢! 1、在coordinateWithContext的例子中,总共有12个子goroutine被创建,第12个即最后一个子goroutine在运行结束时,会通过计算defer表达式从而触发cancelFunc的调用,从而通知主goroutine结束在ctx.Done上获取通道的接收等待。我的问题是,在第12个子goroutine计算defer表达式的时候,会不会存在if条件不满足,未执行到cancelFunc的情况?或者说,在此时,第1到第11的子goroutine中,会存在自旋cas未执行完的情况吗?如果这种情况有,是否会导致主goroutine永远阻塞的情况? 2、在撤销函数被调用的时候,在当前context上,通过contex.Done获取的通道会马上感知到吗?还是会同步等待,使撤销信号在当前context的所有subtree上的所有context传播完成后,再感知到?还是有其他情况? 3、WithDeadline和WithTimeout的区别是什么?具体说,deadline是针对某个具体时间,而timeout是针对当前时间的延时来定义自动撤销时间吗? 感谢回复!
作者回复: 首先要明确: 1. coordinateWithContext函数里的for语句是为了启用12个goroutine,但是这些go函数谁先执行谁后执行与这些goroutine的启用顺序而关。 2. addNum函数里的defer函数会在它后面的for语句执行完毕之后才开始执行。 所以,第一个问题的答案是:不会。因为总会有一个addNum函数把num的值变成12,然后执行deferFunc函数,并由于num==12而执行cancelFunc函数。 另外,这些go函数在调用addNum函数时会碰到自旋的情况(程序会打印出来),但是绝不会造成死锁,因为这些AddNum函数中的CAS操作早晚会执行成功。原子的Load+CAS外加for语句,相当于乐观锁,而且它们的操作都很“规矩”(都只是+1而已)。你也可以理解为把这12次“累加”串行化了,只不过大家是并发的,都在寻找自己“累加”成功的机会。 第二个问题:当前context会马上感知到,但前提是它是可撤销的。通过WithValue函数构造出来的context只会传递不会感知(通过匿名字段实现的)。 第三个问题:你已经把答案说出来了,我就不复述了。
2020-09-0548 - Shawn看代码是深度优先,但是我自己写了demo,顺序是乱的,求老师讲解
作者回复: 打印出来的顺序不定是正常的,因为goroutine会被实时调度啊,打印出来的顺序不一定就是真实顺序。每填语句执行完都可能被调度。
2018-10-258 - mclee实测了下,context.WithValue 得到的新的 ctx 当其 parent context cancle 时也能收到 done 信号啊,并不是文中说的那样会跳过! package main import ( "context" "fmt" "time" ) func main() { ctx1, cancelFun := context.WithCancel(context.Background()) ctx2 := context.WithValue(ctx1, "", "") ctx3, _ := context.WithCancel(ctx1) go watch(ctx1, "ctx1") go watch(ctx2, "ctx2") go watch(ctx3, "ctx3") time.Sleep(2 * time.Second) fmt.Println("可以了,通知监控停止") cancelFun() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name,"监控退出,停止了...") return default: fmt.Println(name,"goroutine监控中...") time.Sleep(2 * time.Second) } } }
作者回复: 我说的跳过是源码级别的跳过,是跳过value节点直接传到它下级的节点,因为value节点本身是没有timeout机制,无需让cancel信号在那里发挥什么作用。 在value节点上的Done()在源码级别实际上调用的并不是value节点自己的方法,而是它上级节点(甚至上上级)的方法。
2022-02-1144 - 茴香根留言区很多人说Context 是深度优先,但是我在想每个goroutine 被调用的顺序都是不确定的,因此在编写goroutine 代码时,实际的撤销响应不能假定其父或子context 所在的goroutine一定先或者后结束。
作者回复: 是的,这涉及到两个方面,需要综合起来看。
2019-07-254 - Cutlercotext.backround()和cotext.todo()有什么区别
作者回复: 很明显,context.Background()返回的是全局的上下文根(我在文章中多次提到),context.TODO()返回的是空的上下文(表明应用的不确定性)。
2019-04-0934 - 鲲鹏飞九万里老师,您还能看到我的留言吗,现在已经是2023年了。您看我下面的代码,比您的代码少了一句time.Sleep(time.Millisecond * 200), 之后,打印的结果就是错的,只打印了12个数,您能给解释一下吗。(我运行环境是:go version go1.18.3 darwin/amd64, 2.3 GHz 四核Intel Core i5) func main() { // coordinateWithWaitGroup() coordinateWithContext() } func coordinateWithContext() { total := 12 var num int32 fmt.Printf("The number: %d [with context.Context]\n", num) cxt, cancelFunc := context.WithCancel(context.Background()) for i := 1; i <= total; i++ { go addNum(&num, i, func() { // 如果所有的addNum函数都执行完毕,那么就立即分发子任务的goroutine // 这里分发子任务的goroutine,就是执行 coordinateWithContext 函数的goroutine. if atomic.LoadInt32(&num) == int32(total) { // <-cxt.Done() 针对该函数返回的通道进行接收操作。 // cancelFunc() 函数被调用,针对该通道的接收会马上结束。 // 所以,这样做就可以实现“等待所有的addNum函数都执行完毕”的功能 cancelFunc() } }) } <-cxt.Done() fmt.Println("end.") } func addNum(numP *int32, id int, deferFunc func()) { defer func() { deferFunc() }() for i := 0; ; i++ { currNum := atomic.LoadInt32(numP) newNum := currNum + 1 // time.Sleep(time.Millisecond * 200) if atomic.CompareAndSwapInt32(numP, currNum, newNum) { fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i) break } else { fmt.Printf("The CAS option failed. [%d-%d]\n", id, i) } } } 运行的结果为: $ go run demo01.go The number: 0 [with context.Context] The number: 1 [12-0] The number: 2 [1-0] The number: 3 [2-0] The number: 4 [3-0] The number: 5 [4-0] The number: 6 [9-0] The number: 7 [10-0] The number: 8 [11-0] The number: 10 [6-0] The number: 11 [5-0] The number: 9 [8-0] end.
作者回复: 从 addNum 成功 +1,到它向标准输出打印内容,这中间是也是有延迟的,而且在那两行代码之间,Go语言的 runtime 也可能会进行调度。所以,部分数字的打印可能会出现乱序,或者直到程序运行结束也没来得及打印出来。加入 time.Sleep(time.Millisecond * 200) 就是为了能让 addNum 执行得慢一点,并且让 CAS 操作多重试几次,这样就更容易让数字的打印呈现出自然序。 要是没有 time.Sleep 的话,就有可能发生这种情况:某个 addNum 已经把 num 加到 12 了,并且已经执行了 cancelFunc,但是其他的 addNum 还没有来得及打印内容。 不过,即使在这种情况下,其他的数字都可能来不及打印,但是 12 肯定是会打印出来的。因为把数字添加到 12 的那个 addNum 一定就是执行 cancelFunc 函数的那个函数,并且打印语句肯定会在 cancelFunc 之前执行。12 可能会出现在“end.”之后,但是肯定是会打印出来的。 你这个打印结果是不是没有贴全?
2023-01-09归属地:北京1 - hunterlodge“由于Context类型实际上是一个接口类型,而context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型,所以,如此传播并不会影响该类型值的功能和安全。” 请问老师,这句话中的「所以」二字怎么理解呢?指针不是会导致数据共享和竞争吗?为什么反而是安全的呢?谢谢!
作者回复: 你引用的这段话其实有两层含义: 1. 某个值的指针值的传递并不会导致源值(即指针指向的那个值)的拷贝。 2. 接口值里面其实会存储实际值的指针值,而不是实际值。所以在拷贝之后,其中的实际值依然是源值。 你看过sync包的文档吗?里面的同步工具大都不允许“使用之后的再传递”。 其实这种约束的原因就是,传递值会导致值的拷贝。如此一来,原值(即拷贝前的值)和拷贝值就是两个(几乎)不相干的值了。在两边分别对它们进行操作,也就起不到同步工具原有的作用了。 但如果是传递指针值的话,两边操作的仍然会是同一个源值,对吧?这就避免了同步工具的“操作失灵”的问题。 你说的“指针会导致数据共享和竞争”是另一个视角的问题。但是这两个问题的底层知识是一个,即:传递指针值的时候并不会拷贝源值,导致分别操作两个指针值相当于在操作同一个源值。 同步工具的内部自有避免竞争的手段,所以拷贝其指针值在大多数情况下是可以的。但是,最好还是不要拷贝它们的指针值(尤其是sync包中的那些工具),因为这样很可能会迷惑住读代码和后续写代码的人,导致理解错误或操作错误的概率很大。
2021-05-051 - moooofly“它会向它的所有子值(或者说子节点)传达撤销信号。这些子值会如法炮制,把撤销信号继续传播下去。最后,这个 Context 值会断开它与其父值之间的关联。”--这里有一个问题,我能理解,当在这个上下文树上的某个 node 上触发 cancel 信号时,以该 node 为根的子上下文树会从原来的树上断开;而文中又提到“撤销信号在被传播时,若遇到它们(调用 context.WithValue 函数得到的 Context 值)则会直接跨过” ,那么,这些被“跨过”的 node ,在上面说的子上下文树断开的过程里,是一起断开了?还是仍旧会和更上层的 node 节点有关联?
作者回复: 逻辑上会一起断开的。但由于 value context 本身不会去传递信号,所以实质上不用做断开操作。这几种 context 所起到的作用是不同的,所以有些专属的操作只会在对应的 context 上做。不过与它们临近的其他种类的 context 会随之联动。你看下源码就清楚了,它们不是完全“链接”在一起的,有的会紧密嵌套在一起,所以有的操作可以很自然地进行跨越式处理。
2019-09-3021 - 海盗船长实际使用中 http.ReverseProxy经常会报 proxy error:context canceled 请问老师有哪些原因可能导致这个问题
作者回复: 你可以说的具体一些。
2019-09-021 - 闫飞繁衍一词的翻译有些生硬,是否能换一个好理解一些的中文词汇
作者回复: 翻译的词?这是我找到的一个比较贴切的词。把 Context 比喻成可以繁衍后代的生物不会更容易理解一些么?
2019-07-161