协程有一个运行特点,是其本身不受其他协程的影响,在编码上如果处理不当,有可能会出现一直驻留在内存区的协程,形成僵尸协程。随着运行时间的迁移,僵尸协程不断增多,会进而影响到系统的稳定性。
将派生出的子Context以参数的形式传入协程方法中,协程中的任务在循环执行的间隔期不断地通过select语句检查该子Context状态的Done通道,如果接收到信号则说明上下文被关闭,当前协程应该退出了,否则会继续执行协程中的任务。
在主方法中,当确认协程需要退出时,会调用Context创建时产生的Cancel()方法,这时该Context和该Context所派生的每个子Context都会在其Done通道中接收到结束信号,协程获取到该结束信号后,就可以终止运行退出协程。
select命令的语法与switch相似,区别是select语法中是判断哪个case的通道最先出队,当所有case通道都阻塞时,整个select语句块也会同样阻塞。为了避免select出现永久阻塞,可以加default语句块,表示在所有的case都无法出队时则运行default中的逻辑。
select {
case <-chan1:
...
case val := <-chan2:
...
default:
...
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
// doSomething 方法中会执行一个循环任务,在每个循环执行的周期内都会检查上下文状态,
// 如果上下文接收到Done信号,则终止任务
func doSomething(ctx context.Context, waitgroup *sync.WaitGroup) {
// 结束方法时释放一个协程计数器
defer func() {
waitgroup.Done()
}()
fmt.Println("start doing jobs.")
// 执行工作任务,用时6秒
for i := 0; i < 6; i++ {
// 执行任务
fmt.Printf("doing job %d\n", i)
time.Sleep(time.Second)
// 检查上下文状态,如果接收到Done信号则终止任务,否则进入下一个循环
select {
case <-ctx.Done():
fmt.Println("stop doing jobs")
return
default:
}
}
fmt.Println("finish jobs.")
}
func main() {
fmt.Println("Hello World")
// 初始化等待组,用于阻塞主方法直到所有协程执行完毕
var waitgroup sync.WaitGroup
// 添加一个协程计数器
waitgroup.Add(1)
// 首先在主方法体中通过context.Background()创建一个上下文,做为根级上下文,只用于派生子上下文,不做其他用途
// 通过根级上下文进行派生,获得一个子上下文以及子上下文的终止方法
ctx, cancel := context.WithCancel(context.Background())
// 执行协程方法,并将上下文和等待组做为参数传入,该方法运行耗时6秒
go doSomething(ctx, &waitgroup)
// 等待3秒后执行子上下文的终止方法,关闭上下文
go func() {
fmt.Println("stop doing jobs in 3 seconds")
time.Sleep(3 * time.Second)
cancel()
}()
// 阻塞主方法直到协程计数器归零
waitgroup.Wait()
fmt.Println("Finished Execution")
}
Hello World
stop doing jobs in 3 seconds
start doing jobs.
doing job 0
doing job 1
doing job 2
doing job 3
stop doing jobs
Finished Execution