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
在这篇里介绍了两个主要功能:

  • 控制超时时间
  • 保存上下文数据