Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

32 | context.Context类型

含数据的Context值
可撤销的Context值
从Context值中获取数据
通过WithValue函数携带数据
通过context包的函数产生的Context值的传播
撤销的原始含义
Err方法
Done方法
WithValue
WithTimeout
WithDeadline
WithCancel
Context值在传达撤销信号的时候的优势和劣势
总结
Context值携带数据
撤销信号的传播
可撤销的含义
context包中的函数
所有的Context值共同构成了一颗代表了上下文全貌的树形结构
可以繁衍出任意个子值
可以提供一类代表上下文的值
可以被传播给多个goroutine
通用的同步工具
使用addNum函数
使用context包中的WithCancel函数和context.CancelFunc类型
使用context包中的函数和Context类型作为实现工具
使用context包中的程序实体实现一对多的goroutine协作流程
使用for循环复用WaitGroup值
分批地启用执行子任务的goroutine的风险
WaitGroup值的计数周期完整性
WaitGroup值的复用
分批地启用执行子任务的goroutine的解决方案
并发地调用Add方法可能引发panic
先统一Add,再并发Done,最后Wait的标准模式
思考题
问题解析
context.Context类型
coordinateWithContext函数
coordinateWithWaitGroup函数
WaitGroup值补充知识
sync.WaitGroup类型
context.Context类型
参考文章

该思维导图由 AI 生成,仅供参考

我们在上篇文章中讲到了sync.WaitGroup类型:一个可以帮我们实现一对多 goroutine 协作流程的同步工具。
在使用WaitGroup值的时候,我们最好用“先统一Add,再并发Done,最后Wait”的标准模式来构建协作流程。
如果在调用该值的Wait方法的同时,为了增大其计数器的值,而并发地调用该值的Add方法,那么就很可能会引发 panic。
这就带来了一个问题,如果我们不能在一开始就确定执行子任务的 goroutine 的数量,那么使用WaitGroup值来协调它们和分发子任务的 goroutine,就是有一定风险的。一个解决方案是:分批地启用执行子任务的 goroutine。

前导内容:WaitGroup 值补充知识

我们都知道,WaitGroup值是可以被复用的,但需要保证其计数周期的完整性。尤其是涉及对其Wait方法调用的时候,它的下一个计数周期必须要等到,与当前计数周期对应的那个Wait方法调用完成之后,才能够开始。
我在前面提到的可能会引发 panic 的情况,就是由于没有遵循这条规则而导致的。
只要我们在严格遵循上述规则的前提下,分批地启用执行子任务的 goroutine,就肯定不会有问题。具体的实现方式有不少,其中最简单的方式就是使用for循环来作为辅助。这里的代码如下:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

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-05
    4
    8
  • Shawn
    看代码是深度优先,但是我自己写了demo,顺序是乱的,求老师讲解

    作者回复: 打印出来的顺序不定是正常的,因为goroutine会被实时调度啊,打印出来的顺序不一定就是真实顺序。每填语句执行完都可能被调度。

    2018-10-25
    8
  • 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-11
    4
    4
  • 茴香根
    留言区很多人说Context 是深度优先,但是我在想每个goroutine 被调用的顺序都是不确定的,因此在编写goroutine 代码时,实际的撤销响应不能假定其父或子context 所在的goroutine一定先或者后结束。

    作者回复: 是的,这涉及到两个方面,需要综合起来看。

    2019-07-25
    4
  • Cutler
    cotext.backround()和cotext.todo()有什么区别

    作者回复: 很明显,context.Background()返回的是全局的上下文根(我在文章中多次提到),context.TODO()返回的是空的上下文(表明应用的不确定性)。

    2019-04-09
    3
    4
  • 鲲鹏飞九万里
    老师,您还能看到我的留言吗,现在已经是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-05
    1
  • moooofly
    “它会向它的所有子值(或者说子节点)传达撤销信号。这些子值会如法炮制,把撤销信号继续传播下去。最后,这个 Context 值会断开它与其父值之间的关联。”--这里有一个问题,我能理解,当在这个上下文树上的某个 node 上触发 cancel 信号时,以该 node 为根的子上下文树会从原来的树上断开;而文中又提到“撤销信号在被传播时,若遇到它们(调用 context.WithValue 函数得到的 Context 值)则会直接跨过” ,那么,这些被“跨过”的 node ,在上面说的子上下文树断开的过程里,是一起断开了?还是仍旧会和更上层的 node 节点有关联?

    作者回复: 逻辑上会一起断开的。但由于 value context 本身不会去传递信号,所以实质上不用做断开操作。这几种 context 所起到的作用是不同的,所以有些专属的操作只会在对应的 context 上做。不过与它们临近的其他种类的 context 会随之联动。你看下源码就清楚了,它们不是完全“链接”在一起的,有的会紧密嵌套在一起,所以有的操作可以很自然地进行跨越式处理。

    2019-09-30
    2
    1
  • 海盗船长
    实际使用中 http.ReverseProxy经常会报 proxy error:context canceled 请问老师有哪些原因可能导致这个问题

    作者回复: 你可以说的具体一些。

    2019-09-02
    1
  • 闫飞
    繁衍一词的翻译有些生硬,是否能换一个好理解一些的中文词汇

    作者回复: 翻译的词?这是我找到的一个比较贴切的词。把 Context 比喻成可以繁衍后代的生物不会更容易理解一些么?

    2019-07-16
    1
收起评论
显示
设置
留言
46
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部