32 | context.Context类型
32 | context.Context类型
讲述:黄洲君
时长15:51大小7.26M
前导内容:WaitGroup 值补充知识
问题解析
知识扩展
问题 1:“可撤销的”在context包中代表着什么?“撤销”一个Context值又意味着什么?
问题 2:撤销信号是如何在上下文树中传播的?
问题 3:怎样通过Context值携带数据?怎样从中获取数据?
总结
思考题
赞 20
提建议
精选留言(45)
- Cloud2018-10-24还没用过context包的我看得一愣一愣的共 4 条评论114
- Spike2018-12-05https://blog.golang.org/pipelines https://blog.golang.org/context 要了解context的来源和用法,建议先阅读官网的这两篇blog61
- 勉才2019-01-01context 树不难理解,context.Background 是根节点,但它是个空的根节点,然后通过它我们可以创建出自己的 context 节点,在这个节点之下又可以创建出新的 context 节点。看了 context 的实现,其实它就是通过一个 channel 来实现,cancel() 就是关闭该管道,context.Done() 在 channel 关闭后,会返回。替我们造的轮子主要实现两个功能:1. 加锁,实现线程安全;2. cancel() 会将本节点,及子节点的 channel 都关闭。37
- Li Yao2018-11-16如果能举一个实际的应用场景就更好了,这篇看不太懂用途共 1 条评论34
- 丁香与茉莉2018-11-30http://www.flysnow.org/2017/05/12/go-in-action-go-context.html共 3 条评论21
- Timo2019-05-28Context 使用原则 不要把Context放在结构体中,要以参数的方式传递 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递 Context是线程安全的,可以放心的在多个goroutine中传递展开14
- 鸠摩·智2020-09-05@郝老师 有几点疑问烦劳回答下,谢谢! 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 条评论7 - 郝林2019-04-17我看不少读者都说写一篇难理解。可能的确如此,因为我假设你们已对context包有基本的了解。 不过没关系,你们有此方面的任何问题都可以通过以下三个途径与我讨论: 1. 直接在专栏文章下留言; 2. 在 GoHackers 微信群或者 BearyChat 中的 GoHackers 团队空间里艾特我; 3. 在知识星球中的 GoHackers VIP 社群里向我提问。展开共 5 条评论7
- 憶海拾貝2018-11-15服务间调用通常要传递上下文数据,用带值Context在每个函数间传递是一种方式,但从Python过来的我觉得这对代码侵入性蛮大的。请问go中还有什么更好的办法传递上下文数据呢?7
- Shawn2018-10-25看代码是深度优先,但是我自己写了demo,顺序是乱的,求老师讲解
作者回复: 打印出来的顺序不定是正常的,因为goroutine会被实时调度啊,打印出来的顺序不一定就是真实顺序。每填语句执行完都可能被调度。
6 - Geek_f396592019-11-08根据这句话:“A great mental model of using Context is that it should flow through your program. Imagine a river or running water. This generally means that you don’t want to store it somewhere like in a struct. Nor do you want to keep it around any more than strictly needed. Context should be an interface that is passed from function to function down your call stack, augmented as needed.” 感觉上Context设计上更像是一个运行时的动态概念,它更像是代表传统call stack 层层镶套外加分叉关系的那颗树。代表着运行时态的调用树。所以它最好就是只存在于函数体的闭包之中,大家口口相传,“传男不传女”。“因为我调用你,所以我才把这个传给你,你可以传给你的子孙,但不要告诉别人!”。所以最好不要把它保存起来让旁人有机会看得到或用到。 楼上有人提到这种风格的入侵性,我能理解你的感觉。但以我以前玩Node.js中的cls (continuation local storage)踩过的那些坑来看,我宁愿两害相权取其轻。这种入侵性至少是可控的,显式的。同步编程的世界我们只需要TLS(Thread local storage)就好了,但对应的异步编程的世界里玩cls很难搞的。在我来看,Context显然是踩过那些坑的老鸟搞出来的。展开5
- mclee2022-02-11实测了下,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 - Direction2020-10-22在 Go 服务中,每个传入的请求都在它自己的 goroutine 中处理。请求处理程序通常启动额外的 goroutine 来访问后端,如数据库和 RPC 服务。 处理请求的 goroutine 集通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当一个请求被取消或超时时(cancelFunc() or WithTimeout()),处理该请求的所 有 goroutines 应该快速退出(ctx.Done()),以便系统可以回收(reclaim)它们正在使用的任何资源。 感觉这个举例挺好的(参考至:https://blog.golang.org/context)展开4
- 茴香根2019-07-25留言区很多人说Context 是深度优先,但是我在想每个goroutine 被调用的顺序都是不确定的,因此在编写goroutine 代码时,实际的撤销响应不能假定其父或子context 所在的goroutine一定先或者后结束。
作者回复: 是的,这涉及到两个方面,需要综合起来看。
4 - Cutler2019-04-09cotext.backround()和cotext.todo()有什么区别
作者回复: 很明显,context.Background()返回的是全局的上下文根(我在文章中多次提到),context.TODO()返回的是空的上下文(表明应用的不确定性)。
共 3 条评论4 - 属雨2018-10-25深度优先,看func (c *cancelCtx) cancel(removeFromParent bool, err error)方法的源代码。3
- HOPE2018-11-12老师,请教个问题。我运行了多个goroutine,每个goroutine有不同的配置。我现在想修改运行时的goroutine的配置,使用context可以实现吗?或者可以用什么办法实现?共 1 条评论2
- 罗志洪2018-10-24context树有点难理解。2
- 鲲鹏飞九万里2023-01-09 来自北京老师,您还能看到我的留言吗,现在已经是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 - hunterlodge2021-05-05“由于Context类型实际上是一个接口类型,而context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型,所以,如此传播并不会影响该类型值的功能和安全。” 请问老师,这句话中的「所以」二字怎么理解呢?指针不是会导致数据共享和竞争吗?为什么反而是安全的呢?谢谢!
作者回复: 你引用的这段话其实有两层含义: 1. 某个值的指针值的传递并不会导致源值(即指针指向的那个值)的拷贝。 2. 接口值里面其实会存储实际值的指针值,而不是实际值。所以在拷贝之后,其中的实际值依然是源值。 你看过sync包的文档吗?里面的同步工具大都不允许“使用之后的再传递”。 其实这种约束的原因就是,传递值会导致值的拷贝。如此一来,原值(即拷贝前的值)和拷贝值就是两个(几乎)不相干的值了。在两边分别对它们进行操作,也就起不到同步工具原有的作用了。 但如果是传递指针值的话,两边操作的仍然会是同一个源值,对吧?这就避免了同步工具的“操作失灵”的问题。 你说的“指针会导致数据共享和竞争”是另一个视角的问题。但是这两个问题的底层知识是一个,即:传递指针值的时候并不会拷贝源值,导致分别操作两个指针值相当于在操作同一个源值。 同步工具的内部自有避免竞争的手段,所以拷贝其指针值在大多数情况下是可以的。但是,最好还是不要拷贝它们的指针值(尤其是sync包中的那些工具),因为这样很可能会迷惑住读代码和后续写代码的人,导致理解错误或操作错误的概率很大。
1