内容简介:sync.WaitGroup类型是一个实现一对多goroutine协作流程的同步工具。还有另一种工具也可以实现这种协作流程。在使用WaitGroup的时候,建议是用“先统一Add,再并发Done,最后Wait”的模式来构建协作流程。要避免并发的调用Add方法。这就带来一个问题,需要在一开始就能确定执行子任务的goroutine的数量,至少也是在启动goroutine之前。下面是一个示例,稍微做了一些改造:
context.Context
sync.WaitGroup类型是一个实现一对多goroutine协作流程的同步工具。还有另一种 工具 也可以实现这种协作流程。
回顾sync.WaitGroup实现协作流程
在使用WaitGroup的时候,建议是用“先统一Add,再并发Done,最后Wait”的模式来构建协作流程。要避免并发的调用Add方法。这就带来一个问题,需要在一开始就能确定执行子任务的goroutine的数量,至少也是在启动goroutine之前。
下面是一个示例,稍微做了一些改造:
package main import ( "time" "fmt" "sync" "sync/atomic" ) func coordinateWithWaitGroup() { total := 12 var num int32 var wg sync.WaitGroup // 定义好goroutine中返回前要执行的defer函数 deferFunc := func() { wg.Done() } for i := 0; i < total; i++ { wg.Add(1) go addNum(&num, i, deferFunc) } wg.Wait() } // 这个函数的defer函数通过参数来给出 func addNum(numP *int32, id int, deferFunc func()) { defer deferFunc() for i := 1; ; i++ { currNum := atomic.LoadInt32(numP) newNum := currNum + 1 time.Sleep(time.Millisecond * 200) if atomic.CompareAndSwapInt32(numP, currNum, newNum) { fmt.Printf("id: %02d 第 %02d 次更新num成功: %d\n", id, i, newNum) break } } } func main() { coordinateWithWaitGroup() }
这里的改造是为了更像之后要使用context包时的用法,不过在使用规则上还是满足WaitGroup的要求的。
通过context包实现协作流程
这里就是要在写一个coordinateWithWaitContext函数,来代替上面的coordinateWithWaitGroup函数。两个函数要具有相同的功能。
这里先直接给出示例代码了:
func coordinateWithWaitContext() { total := 12 var num int32 cxt, cancelFunc := context.WithCancel(context.Background()) // 定义好goroutine中返回前要执行的defer函数,这里用到了上面的cancelFunc deferFunc := func() { if atomic.LoadInt32(&num) == int32(total) { cancelFunc() } } for i := 0; i < total; i++ { go addNum(&num, i, deferFunc) } <- cxt.Done() }
所有的变化都在上面这个函数里了。这里先后调用了context.Background函数和context.WithCancel函数。得到了一个可撤销context.Context类型的值,赋值给了变量cxt。还有一个context.CancelFunc类型的撤销函数,赋值给了变量cancelFunc。
这里在判断goroutine执行完毕的依据是通过判断num里的值。一旦判断完成,就会调用之前准备好的cancelFunc函数,此时cxt.Done函数返回的通道就会接收到值,结束等待。
和WaitGroup的比较
WaitGroup需要事先知道所有goroutine的数量,而context这里更关心是否满足某个条件,一旦条件满足就可以退出。
这里我想提一下python,让我想到了 python 中的for循环和while循环。能用for循环就不要用while循环。使用while循环可能由于条件判断复杂了,造成条件永远无法满足而成了死循环。使用for循环的话就没有这个问题了。不过当循环的退出和数量没有关系时,只能用while循环了。
就好比WaitGroup,如果可以通过goroutine的数量判断,那么应该还是使用WaitGroup好。如果遇到结束条件和goroutine数量无关的时候,就只能用context了。
context.Context类型
context.Context类型,是在 Go 1.7发布时才被加入到标准库的。而后,标准库中的很多其他代码包都为了支持它而进行了扩展,包括:os/exec包、net包、database/sql包、runtime/pprof包和runtime/trace包,等等。
之所以会收到众多代码包的积极支持,主要因为它是一种非常通用的同步工具。它的值不但可以任意的扩散,而且还可以被用来传递额外的信息和信号。就是Context类型可以提供一个代表上下文的值,之类值是并发安全的,也就是说它可以被传播给多个goroutine。
接口类型Context最新实际是一个接口类型,在context包中实现该接口的所有私有类型,都是基于某个数据类型的指针类型。所以,如此传播并不会影响该类型值的功能和安全。
可繁衍的
Context类型的值是可以繁衍的,这意味着可以通过一个Context值产生出任意个子值。这些子值可以携带父值的属性和数据,也可以相应通过其父值传达的信号。如此,所有的Context值共同构成了一颗代表了上下文全貌的属性结构。树的根节点是一个已经在context包中预定义好的context值,它是全局唯一的。通过调用context.Background函数,就可以获取到它。
包内的函数
在context包中,包含了4个用于繁衍Context值的函数:
- WithCancel,产生一个可撤销的parent的子值
- WithDeadline,产生一个会定时撤销的parent的子值
- WithTimeout,同上,也是定时撤销的parent的子值
- WithValue,产生一个会携带额外数据的parent的子值
这些函数的第一个参数类型都是context.Context,而名称都为parent。顾名思义,这个位置上的参数对应的都是产生Context值的父值。
撤销信号在上下文树中的传播
context包中的WithCancel、WithDeadline和WithTimeout都是被用来基于给定的COntext值产生可撤销的子值的。
这个函数在被调用后,产生两个结果值。第一个是可撤销的Context值,第二个是用于触发撤销信号的函数。
撤销函数被调用后,对应的Context值会先关闭它内部的接收通道,通道关闭了接收该通道的操作就会立即返回,就是Done方法返回的那个通道。然后,它还会向它的所有子值传达撤销信号。这些子值如果还有子值,就会一级一级把撤销信号传递下去。最后,这个Context值会断开它与其父值之间的关联。
通过调用WithDeadline函数或者WithTimeout函数生成的Context值也是可撤销的。它们不但可以被手动撤销,还会依据在生成是给定的过期时间,自动地进行定时撤销。这里的定时撤销功能是借助它们内部的计时器来实现的。
当过期时间到达时,两种Context值的行为与手动撤销是的行为是几乎一致的,只是多了一步停止并释放掉内部的计时器。
WithDeadline和WithTimeout是相似的。都是通过设置,会在某个时间自动触发,就是ctx.Done()能够取到值。差别是,DeadLine是设置一个时间点,时间对上了就到期。Timeout是设置一段时间,比如几秒,过个这段时间,就超时。其实底层的Timeout也是通过Deadlin实现的。
WithValue这个函数得到的值是不可撤销的。撤销信号在传播时,若遇到它们会直接跨过,并试图将信息直接传给它们的子值。
传递数据
通过WithValue函数产生新的Context值的时候需要3个参数:父值、键和值。这里键必须是可判断等的,类似字典的键。不过Context值并不是用字典来存储键和值的,而是简单地存储在父值相应的字段中。
通过Value方法,可以获取数据。在调用包含属性的Context值的Value方法是,会先判断给定的键,如有有就返回存储的值,否则会到其父值中继续查找,会一直沿着上下文根节点的方法一直查找。因为其他几种Context值都是无法携带数据的,所以Value方法在查找的时候,会跨过这这些Context值。
无法改变数据
Context接口没有提供改变数据的方法,所以通常只能通过在上下文数中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃掉相应的数据。如果存储在这里的数据可以从外部改变,那么必须自信保证安全。
下面这个示例展示了Context值里数据的传递:
package main import ( "context" "fmt" "time" ) type myKey int func main() { keys := []myKey{ myKey(20), myKey(30), myKey(60), myKey(61), } values := []string{ "value in node2", "value in node3", "value in node6", "value in node6Branch", } rootNode := context.Background() node1, cancelFunc1 := context.WithCancel(rootNode) defer cancelFunc1() node2 := context.WithValue(node1, keys[0], values[0]) node3 := context.WithValue(node2, keys[1], values[1]) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[0], node3.Value(keys[0])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[1], node3.Value(keys[1])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[2], node3.Value(keys[2])) fmt.Println() node4, cancelFunc4 := context.WithCancel(node3) defer cancelFunc4() node5, cancelFunc5 := context.WithTimeout(node4, time.Hour) defer cancelFunc5() fmt.Printf("The value of the key %v found in the node5: %v\n", keys[0], node5.Value(keys[0])) fmt.Printf("The value of the key %v found in the node5: %v\n", keys[1], node5.Value(keys[1])) fmt.Println() node6 := context.WithValue(node5, keys[2], values[2]) fmt.Printf("The value of the key %v found in the node6: %v\n", keys[0], node6.Value(keys[0])) fmt.Printf("The value of the key %v found in the node6: %v\n", keys[2], node6.Value(keys[2])) fmt.Println() node6Branch := context.WithValue(node5, keys[3], values[3]) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[1], node6Branch.Value(keys[1])) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[2], node6Branch.Value(keys[2])) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[3], node6Branch.Value(keys[3])) fmt.Println() node7, cancelFunc7 := context.WithCancel(node6) defer cancelFunc7() node8, cancelFunc8 := context.WithTimeout(node7, time.Hour) defer cancelFunc8() fmt.Printf("The value of the key %v found in the node8: %v\n", keys[1], node8.Value(keys[1])) fmt.Printf("The value of the key %v found in the node8: %v\n", keys[2], node8.Value(keys[2])) fmt.Printf("The value of the key %v found in the node8: %v\n", keys[3], node8.Value(keys[3])) }
总结
Context类型是一个可以实现多goroutine协作流程同步的工具。还可以通过它的值传达撤销信号或传递数据。
Context类型的值大体可分3种:
- 根Context值
- 可撤销的Context值
- 含数据的Context值
所有的Context值共同构成了一颗上下文树。这棵树的作用域是全局的,根Context值就是树的根,它也是全局唯一的,并且不提供任何额外的功能。
包含数据的Context值不能被撤销,可撤销的Context值又无法携带数据。但是,由于它们共同组成了一个有机的整体,即上下文数,所以在功能上要比sync.WaitGroup强大的多。
这个系列偏重理论,就少了很多实际的应用,关于context包,我之前还有一篇:
http://blog.51cto.com/steed/2330218 在这篇里介绍了两个主要功能:
- 控制超时时间
- 保存上下文数据
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。