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

32 | context.Context类型

我们在上篇文章中讲到了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 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(45)

  • 最新
  • 精选
  • 拂尘
    @郝老师 有几点疑问烦劳回答下,谢谢! 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只会传递不会感知(通过匿名字段实现的)。 第三个问题:你已经把答案说出来了,我就不复述了。

    4
    8
  • Shawn
    看代码是深度优先,但是我自己写了demo,顺序是乱的,求老师讲解

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

    7
  • 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节点自己的方法,而是它上级节点(甚至上上级)的方法。

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

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

    4
  • Cutler
    cotext.backround()和cotext.todo()有什么区别

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

    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.”之后,但是肯定是会打印出来的。 你这个打印结果是不是没有贴全?

    归属地:北京
    1
  • hunterlodge
    “由于Context类型实际上是一个接口类型,而context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型,所以,如此传播并不会影响该类型值的功能和安全。” 请问老师,这句话中的「所以」二字怎么理解呢?指针不是会导致数据共享和竞争吗?为什么反而是安全的呢?谢谢!

    作者回复: 你引用的这段话其实有两层含义: 1. 某个值的指针值的传递并不会导致源值(即指针指向的那个值)的拷贝。 2. 接口值里面其实会存储实际值的指针值,而不是实际值。所以在拷贝之后,其中的实际值依然是源值。 你看过sync包的文档吗?里面的同步工具大都不允许“使用之后的再传递”。 其实这种约束的原因就是,传递值会导致值的拷贝。如此一来,原值(即拷贝前的值)和拷贝值就是两个(几乎)不相干的值了。在两边分别对它们进行操作,也就起不到同步工具原有的作用了。 但如果是传递指针值的话,两边操作的仍然会是同一个源值,对吧?这就避免了同步工具的“操作失灵”的问题。 你说的“指针会导致数据共享和竞争”是另一个视角的问题。但是这两个问题的底层知识是一个,即:传递指针值的时候并不会拷贝源值,导致分别操作两个指针值相当于在操作同一个源值。 同步工具的内部自有避免竞争的手段,所以拷贝其指针值在大多数情况下是可以的。但是,最好还是不要拷贝它们的指针值(尤其是sync包中的那些工具),因为这样很可能会迷惑住读代码和后续写代码的人,导致理解错误或操作错误的概率很大。

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

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

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

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

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

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

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