使用WaitGroup可以实现一对多的goroutine协作流程同步,如果一开始不能确定子任务的goroutine数量,那么使用WaitGroup值来协调它们和分发子任务的goroutine就存在一定的风险。
一个解决方案是:分批地启用执行子任务的goroutine。
func coordinateWithContext() {
total := 12
var num int32
fmt.Printf("The number: %d [with context.Context]\n", num)
// 调用context.Background和context.WithCancel创建一个可撤销的context对象ctx和一个撤销函数cancelFunc
ctx, cancelFunc := context.WithCancel(context.Background())
for i := 1; i <= total; i++ {
// 每次迭代创建一个新的goroutine
go addNum(&num, i, func() {
// 在子goroutine中原子性的Load num变量
if atomic.LoadInt32(&num) == int32(total) {
// 如果num与total相等,表示所有子goroutine执行完成
// 调用context的撤销函数
cancelFunc()
}
})
}
// 调用Done函数,并试图针对该函数返回的通道进行接收操作
// 一旦cancelFunc被调用,针对该通道的接收操作就会马上结束
<-cxt.Done()
fmt.Println("End.")
}
context.Context
类型在1.7版本引入后,许多标准库都进行了扩展支持,包括:os/exec
,net
,database/sql
,runtime/pprof
,runtime/trace
。
context类型是一种非常通用的同步工具,它的值不但可以被任意地扩散,而且还可以被用来传递额外的信息和信号。更具体的说,Context类型可以提供一类代表上下文的值,此类值是并发安全的,可以被传播到多个goroutine。
Context值共同构成了一棵代表了上下文全貌的树形结构。这棵树的树根(上下文根节点)是一个已经在context包中预定义好的Context值,它是全局唯一的。通过调用context.Background函数可以获取它。此处的上下文根节点只是最基本的支点,不通过任何额外的功能,既不能被撤销也不能携带任何数据。
context包中包含四个用于衍生context值的函数:
WithCancel
:产生一个可撤销的parent的子值WithDeadline
,WithTimeout
:产生一个会定时撤销的parent的子值WithValue
:产生一个会携带额外数据的parent的子值这些函数的第一个参数类型都是context.Context
,名称都是parent,这个位置上的参数都是它们将产生的Context值的父值。
Context接口类型中有两个方法与撤销相关:
struct{}
的接收通道,这个通道的用途不是传递元素值,而是让调用方去感知撤销当前Context值的那个信号。一旦当前Context值被撤销,接收通道会立即关闭,(对于一个未包含任何元素值的通道,它的关闭使任何针对它的接收操作立即结束)。context.Canceled
变量的值:表示手动撤销context.DeadlineExceeded
变量的值:表示给定的过期时间已到而导致撤销context.WithCancel
函数产生一个可撤销的Context值,还会获得一个用于出发撤销信号的函数,通过调用该函数,可以触发针对这个Context值的撤销信号,一旦触发,撤销信号会立即被传达给这个Context值,并由它的Done方法的结果值(一个接收通道)表达出来。
撤销函数只负责触发信号,对应的可撤销的Context值也只负责传达信号,它们不会管后边具体的撤销操作,代码在感知到撤销信号后,可以进行任意的操作,Context值对此并没有任何的约束。
更进一步,这里的撤销最原始的含义是:
这是创建Context包和Context类型时的初衷。
Context包中包含四个用于衍生Context值的函数,其中的WithCancel
,WithDeadline
,WithTimeout
都是被用来基于给定的Context值产生可撤销的子值。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 返回一个可被撤销的context值和一个出触发撤销信号的函数
撤销函数被调用后,Context值会先关闭它内部的接收通道,即Done方法会返回的那个通道。然后,它会向它的所有子值(或者说子节点)传达撤销信号。这些子值会继续把撤销信号传播下去,最后这个context值会断开它与其父值之间的关联。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithDeadline
和WithTimeout
函数生成的Context值也是可撤销的,它们不但可以被手动撤销,还会依据在生成时被给定的过期时间,自动地进行定时撤销(定时撤销的功能借助内部的计时器来实现),撤销的同时释放内部的计时器。
注意,通过调用context.WithValue
函数得到的Context值是不可撤销的。撤销信号在被传播时,如遇到它们则会直接跨过,并试图将信号直接传给它们的子值。
func WithValue(parent Context, key, val interface{}) Context
WithValue函数在产生新的包含数据的Context值的时候,需要三个参数,即:父值、键(与map对键的约束类似,类型必须可判等)、值。因为从中获取数据的时候,根据给定的键来查找对应的值。不过这里并没有用map来存储数据。
Context类型的Value方法就是被用来获取数据的,在调用含数据的Context值的Value方法时,它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。如果父值中仍未存储相等的键,那么继续向上直到查找到根节点。
除了包含数据的Context可以存储数据,其他的Context值都不能携带数据,Context的Value方法在向上查找的过程中会直接跳过这几种类型的Context值。
如果调用的Value方法所属的Context本身就不包含数据,那么实际调用的就会是其父值的Value方法。因为这几种Context值的实际类型是结构体,它们通过将父值嵌入到自身来表达父子关系。
Context接口并没有提供改变数据的方法,因此在通常情况下,只能通过上下文树中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃相应的数据。如果存储在这里的数据可以从外部改变,那么必须自行保证安全。
Context类型的实际值分为三种:
所有的Context值共同构成一颗上下文树,这棵树的作用域是全局的,根Context值是全局唯一的,不提供任何额外的功能。
撤销操作是Context值能够协调多个goroutine的关键,撤销信号总是会沿着上下文树叶子节点的方向传播。
含数据的Context不能被撤销,能被撤销的Context无法携带数据,它们共同组成一个整体(上下文树)。