我们已经知道WaitGroup
可以用于并发控制,但当遇到更复杂的场景时,例如主动取消goroutine或者使超时的goroutine自动退出等,WaitGroup
就无能为力。
这个时候,就是context
大有用武之地。
包context
定义了Context
类型,它跨API边界和进程之间携带截止日期,取消信号和其他请求范围的值。
对服务器的传入请求应创建一个Context,对服务器的传出调用应接受一个Context。它们之间的函数调用链必须传播Context,或者传递使用WithCancel,WithDeadline,WithTimeout或WithValue创建的派生Context。当取消一个context
后,所有从这个context
派生的context
也会被取消。
WithCancel,WithDeadline和WithTimeout函数接受Context(父)并返回派生的Context(子)和CancelFunc
。调用CancelFunc
会取消子项及其子项,删除父项对子项的引用,并停止任何关联的计时器。未能调用CancelFunc
会泄漏子项及其子项,直到取消父项或计时器触发。 go vet工具检查CancelFuncs
是否在所有控制流路径上使用。
使用Contexts
的程序应遵循这些规则,以使各包之间的接口保持一致,并启用静态分析工具来检查上下文传播:
- 不要将
Contexts
存储在结构类型中;相反,将Context明确传递给需要它的每个函数。Context应该是第一个参数,通常命名为ctx:func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
- 即使函数允许,也不要传递nil
Context
。如果您不确定要使用哪个Context
,请传递context.TODO。 - 仅将
context values
用于进程间和跨API的请求范围数据,而不是将其作为可选参数传递给函数。 - 可以将相同的Context传递给在不同goroutine中运行的函数;上下文对于多个goroutine同时使用是安全的。
以上说明来自context
包说明,如果感觉翻译的不够清楚,可以查看context
的说明。(实在翻译不下去了 = =!)
Context
定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
说了这么多,现在来看下例子吧。
并发控制 Cancel Example
通过使用WithCancel可以主动取消一个或多个goroutine的执行,以实现对并发的控制。
package main
import (
"context"
"fmt"
"time"
)
func PrintTask(ctx context.Context) {
for {
select {
case <- ctx.Done():
return
default:
time.Sleep(2*time.Second)
fmt.Println("A man must walk down many roads.")
}
}
}
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go PrintTask(ctx)
go PrintTask(ctx)
go PrintTask(ctx)
time.Sleep(3*time.Second)
fmt.Println("main exit...")
}
WithCancel函数原型如下
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel返回两个结果,一个是context(parent的副本,并带有新的Done
channel), 一个是cancel函数。
当调用cancel函数或者parent的Done
channel关闭时,新返回的Done
channel就会被关闭。
例子中,调用cancel函数,关闭了Done
channel后,进而PrintTask就会结束。
output
A man must walk down many roads.
A man must walk down many roads.
A man must walk down many roads.
main exit...
并发超时控制 Timeout Example
WithTimeout可以实现并发超时控制,使goroutine执行超时时自动结束。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
timeout := 50*time.Millisecond
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
done := make(chan int,1)
go func() {
// do something
time.Sleep(1*time.Second)
done<- 1
}()
select{
case <-done:
fmt.Println("work done on time")
case <-ctx.Done():
// timeout
fmt.Println(ctx.Err())
}
fmt.Println("main exit...")
}
例子中,使用WithTimeout()
函数,其实现上,直接调用
WithDeadline(parent, time.Now().Add(timeout))
WithDeadline
函数定义如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
传入参数:
Context
类型的变量parent
timeout
超时时间
返回两个结果:
Context
类型的变量- 取消函数cancel
当出现超时,或者调用取消函数cancel,或者parent
的Done
channel被关闭时,
新返回的context的Done
channel将被关闭。
output:
context deadline exceeded
main exit...