使用context取消goroutine执行
Go语言里每一个并发的执行单元叫做goroutine,当一个用Go语言编写的程序启动时,其main函数在一个单独的goroutine中运行。main函数返回时,所有的goroutine都会被直接打断,程序退出。除此之外如果想通过编程的方法让一个goroutine中断其他goroutine的执行,只能是通过在多个goroutine间通过context上下文对象同步取消信号的方式来实现。
在 Go 语言中,使用 context.Context 对象在 goroutine 之间同步取消信号,是一种非常常见和推荐的方式,来实现一个 goroutine 中断其他 goroutine 的执行。
在 Go 语言的并发编程中,context.Context 对象提供了一种优雅的方式来管理和取消 goroutine 的生命周期。通过在多个 goroutine 中共享同一个 Context 对象,我们可以在任何一个 goroutine 中触发 Context 的取消操作,这个取消信号就会被传递到所有使用该 Context 的 goroutine 中。
取消功能需要从两方面实现才能完成:
- 监听取消事件
- 发出取消事件
监听取消事件
Go语言context标准库的Context类型提供了一个Done()
方法,该方法返回一个类型为<-chan struct{}
的channel
。每次context收到取消事件后这个channel都会接收到一个struct{}类型的值。所以在Go语言里监听取消事件就是等待接收<-ctx.Done()
。
举例来说,假设一个HTTP服务器需要花费两秒钟来处理一个请求。如果在处理完成之前请求被取消,我们想让程序能立即中断不再继续执行下去:
func main() {
// 创建一个监听8000端口的服务器
http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 输出到STDOUT展示处理已经开始
fmt.Fprint(os.Stdout, "processing request\n")
// 通过select监听多个channel
select {
case <-time.After(2 * time.Second):
// 如果两秒后接受到了一个消息后,意味请求已经处理完成
// 我们写入"request processed"作为响应
w.Write([]byte("request processed"))
case <-ctx.Done():
// 如果处理完成前取消了,在STDERR中记录请求被取消的消息
fmt.Fprint(os.Stderr, "request cancelled\n")
}
}))
}
发出取消事件(go context之WithCancel)
如果你有一个可以取消的操作,则必须通过context发出取消事件。可以通过context包的WithCancel函数返回的取消函数来完成此操作(withCancel还会返回一个支持取消功能的上下文对象)。该函数不接受参数也不返回任何内容,当需要取消上下文时会调用该函数,发出取消事件。
- WithCancel()函数接受一个 Context 并返回其子Context和取消函数cancel
- 新创建协程中传入子Context做参数,且需监控子Context的Done通道,若收到消息,则退出
- 需要新协程结束时,在外面调用 cancel 函数,即会往子Context的Done通道发送消息
- 注意:当 父Context的 Done() 关闭的时候,子 ctx 的 Done() 也会被关闭
实验步骤
-
利用根Context创建一个父Context,使用父Context创建一个协程,
-
利用上面的父Context再创建一个子Context,使用该子Context创建一个协程
-
一段时间后,调用父Context的cancel函数,会发现父Context的协程和子Context的协程都收到了信号,被结束了
代码如下
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 父context(利用根context得到)
ctx, cancel := context.WithCancel(context.Background())
// 父context的子协程
go watch1(ctx)
// 子context,注意:这里虽然也返回了cancel的函数对象,但是未使用
valueCtx, _ := context.WithCancel(ctx)
// 子context的子协程
go watch2(valueCtx)
fmt.Println("现在开始等待3秒,time=", time.Now().Unix())
time.Sleep(3 * time.Second)
// 调用cancel()
fmt.Println("等待3秒结束,调用cancel()函数")
cancel()
// 再等待5秒看输出,可以发现父context的子协程和子context的子协程都会被结束掉
time.Sleep(5 * time.Second)
fmt.Println("最终结束,time=", time.Now().Unix())
}
// 父context的协程
func watch1(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,父context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("父context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
// 子context的协程
func watch2(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,子context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("子context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}