在 Go http包的Server中,每一个请求在都有一个对应的 goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和RPC服务。一个上游服务通常需要访问多个下游服务,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
问题:只有所有的goroutine都结束了才算结束,只要有一个goroutine没有结束, 那么就会一直等,这显然对资源的释放是缓慢的
var wg sync.WaitGroup func run(task string) { fmt.Println(task, "start。。。") time.Sleep(time.Second * 2) // 每个goroutine运行完毕后就释放等待组的计数器 wg.Done() } func main() { wg.Add(2) // 需要开启几个goroutine就给等待组的计数器赋值为多少,这里为2 for i := 1; i < 3; i++ { taskName := "task" + strconv.Itoa(i) go run(taskName) } // 等待,等待所有的任务都释放 等待组计数器值为 0 wg.Wait() fmt.Println("所有任务结束。。。") } /* -----------------------运行结果---------------------------- task2 start。。。 task1 start。。。 所有任务结束。。。 */ 上面例子中,一个任务结束了必须等待另外一个任务也结束了才算全部结束了,先完成的必须等待其他未完成的,所有的goroutine都要全部完成才OK。等待组比较适用于好多个goroutine协同做一件事情的时候,因为每个goroutine做的都是这件事情的一部分,只有全部的goroutine都完成,这件事情才算完成;缺点:实际生产中,需要我们主动的通知某一个goroutine结束。eg我们可以设置全局变量,在我们需要通知goroutine要停止的时候,我们为全局变量赋值,但是这样我们必须保证线程安全,不可避免的我们要为全局变量加锁,在便利性及性能上稍显不足通过在main goroutine中像chan中发送关闭停止指令,并配合select,从而达到关闭goroutine的目的,这种方式显然比等待组优雅的多,但是在goroutine中在嵌套goroutine的情况就变得异常复杂。
func main() { stop := make(chan bool) // 开启goroutine go func() { for { select { case <- stop: fmt.Println("任务1 结束了。。。") return default: fmt.Println(" 任务1 正在运行中。") time.Sleep(time.Second * 2) } } }() // 运行10s后停止 time.Sleep(time.Second * 10) fmt.Println("需要停止任务1。。。") stop <- true time.Sleep(time.Second * 1) } /* ------------------执行结果--------------------------------- 任务1 正在运行中... 任务1 正在运行中... 任务1 正在运行中... 任务1 正在运行中... 任务1 正在运行中... 任务1 正在运行中... 需要停止任务1... 任务1 结束了... */ 劣势:如果有很多 goroutine 都需要控制结束和如果这些 goroutine 又衍生了其它更多的goroutine比较麻烦。上面例子中,启动了 3 个监控 goroutine进行不断的运行任务,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这 3 个 goroutine 都会被结束。canel之后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放 goroutine,这就优雅的解决了 goroutine 启动后不可控的问题。
Go帮我们实现了2个Context接口,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } / type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil } Background()主要用于main 函数、初始化以及测试代码中,作为 Context这个树结构的最顶层的 Context,也就是根Context。TODO(),它目前还不知道具体的使用场景…它们两个本质上都是 emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。context 包为我们提供的 With 系列的函数,可以让我们在原来的Context上衍生出子Context。
//返回子 Context,以及一个取消函数用来取消 Context。 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //传入截止时间参数,意味着到了这个时间点,会自动取消 Context,也可以不等到这个时候,可以提前通过取消函数进行取消。 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //传入一个时间参数,多少时间之后取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) //生成一个绑定了一个键值对数据的 Context,即给context设置值,这个绑定的数据可以通过 Context.Value 方法访问到. func WithValue(parent Context, key, val interface{}) Context CancelFunc func(),该函数可以取消一个Context,以及这个节点 Context下所有的所有的 Context,不管有多少层级。context.WithValue方法附加一对 K-V 的键值对,这里 Key 必须是等价性的,也就是具有可比性;Value值要是线程安全的。在使用值的时候,可以通过 Value方法读取: ctx.Value(key)。使用WithValue 传值,一般是必须的值,不要什么值都传递。参考