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值产生可撤销的子值的。
WithCancel
这个函数在被调用后,产生两个结果值。第一个是可撤销的Context值,第二个是用于触发撤销信号的函数。
撤销函数被调用后,对应的Context值会先关闭它内部的接收通道,通道关闭了接收该通道的操作就会立即返回,就是Done方法返回的那个通道。然后,它还会向它的所有子值传达撤销信号。这些子值如果还有子值,就会一级一级把撤销信号传递下去。最后,这个Context值会断开它与其父值之间的关联。
WithDeadline和WithTimeout
通过调用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
在这篇里介绍了两个主要功能:
- 控制超时时间
- 保存上下文数据
有疑问加站长微信联系(非本文作者)