内容简介:这篇文章将介绍一些使用简单来说,我们需要取消功能来防止系统做一些不必要的工作。
Go
语言里每一个并发的执行单元叫做 goroutine
,当一个用 Go
语言编写的程序启动时,其 main
函数在一个单独的 goroutine
中运行。 main
函数返回时,所有的 goroutine
都会被直接打断,程序退出。除此之外如果想通过编程的方法让一个 goroutine
中断其他 goroutine
的执行,只能是通过在多个 goroutine
间通过 context
上下文对象同步取消信号的方式来实现。
这篇文章将介绍一些使用 context
对象同步信号取消中断程序执行的常用模式和最佳实践,从而让我们能构建更迅捷、健壮的应用程序。如果对context对象不太了解的同学建议先仔细看看《 Golang 并发编程之Context 》了解一下基础。
为什么需要取消功能
简单来说,我们需要取消功能来防止系统做一些不必要的工作。
考虑以下常见的场景:一个 HTTP
服务器查询数据库并将查询到的数据作为响应返回给客户端:
如果一切正常,时序图将如下所示:
但是,如果客户端在中途取消了请求会发生什么?这种情况可以发生在,比如用户在请求中途关闭了浏览器。如果不支持取消功能, HTTP
服务器和数据库会继续工作,由于客户端已经关闭所以他们工作的成果也就被浪费了。这种情况的时序图如下所示:
理想情况下,如果我们知道某个处理过程(在此示例中为HTTP请求)已停止,则希望该过程的所有下游组件都停止运行:
使用context实现取消功能
现在我们知道了应用程序为什么需要取消功能,接下来我们开始探究在 Go
中如何实现它。因为“取消事件”与正在执行的操作高度相关,因此很自然地会将它与上下文捆绑在一起。
取消功能需要从两方面实现才能完成:
- 监听取消事件
- 发出取消事件
监听取消事件
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") } })) }
你可以通过运行服务器并在浏览器中打开 localhost:8000
进行测试。如果你在2秒钟前关闭浏览器,则应该在终端窗口上看到“request cancelled”字样。
发出取消事件
如果你有一个可以取消的操作,则必须通过 context
发出取消事件。可以通过 context
包的 WithCancel
函数返回的取消函数来完成此操作( withCancel
还会返回一个支持取消功能的上下文对象)。该函数不接受参数也不返回任何内容,当需要取消上下文时会调用该函数,发出取消事件。
考虑有两个相互依赖的操作的情况。在这里,“依赖”是指如果其中一个失败,那么另一个就没有意义,而不是第二个操作依赖第一个操作的结果(那种情况下,两个操作不能并行)。在这种情况下,如果我们很早就知道其中一个操作失败,那么我们就会希望能取消所有相关的操作。
func operation1(ctx context.Context) error { // 让我们假设这个操作会因为某种原因失败 // 我们使用time.Sleep来模拟一个资源密集型操作 time.Sleep(100 * time.Millisecond) return errors.New("failed") } func operation2(ctx context.Context) { // 我们使用在前面HTTP服务器例子里使用过的类型模式 select { case <-time.After(500 * time.Millisecond): fmt.Println("done") case <-ctx.Done(): fmt.Println("halted operation2") } } func main() { // 新建一个上下文 ctx := context.Background() // 在初始上下文的基础上创建一个有取消功能的上下文 ctx, cancel := context.WithCancel(ctx) // 在不同的goroutine中运行operation2 go func() { operation2(ctx) }() err := operation1(ctx) // 如果这个操作返回错误,取消所有使用相同上下文的操作 if err != nil { cancel() } }
基于时间的取消
任何需要在请求的最大持续时间内维持SLA(服务水平协议)的应用程序,都应使用基于时间的取消。该API与前面的示例几乎相同,但有一些补充:
// 这个上下文将会在3秒后被取消 // 如果需要在到期前就取消可以像前面的例子那样使用cancel函数 ctx, cancel := context.WithTimeout(ctx, 3*time.Second) // 上下文将在2009-11-10 23:00:00被取消 ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
例如,程序在对外部服务进行 HTTP API
调用时设置超时时间。如果被调用服务花费的时间太长,到时间后就会取消请求:
func main() { // 创建一个超时时间为100毫秒的上下文 ctx := context.Background() ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond) // 创建一个访问Google主页的请求 req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil) // 将超时上下文关联到创建的请求上 req = req.WithContext(ctx) // 创建一个HTTP客户端并执行请求 client := &http.Client{} res, err := client.Do(req) // 如果请求失败了,记录到STDOUT if err != nil { fmt.Println("Request failed:", err) return } // 请求成功后打印状态码 fmt.Println("Response received, status code:", res.StatusCode) }
根据 Google
主页响应你请求的速度,你将收到:
Response received, status code: 200
或者:
Request failed: Get http://google.com: context deadline exceeded
对于我们来说通常都会收到第二条消息:)
context使用上的一些陷阱
尽管 Go
中的上下文取消功能是一种多功能工具,但是在继续操作之前,你需要牢记一些注意事项。其中最重要的是,上下文只能被取消一次。如果您想在同一操作中传播多个错误,那么使用上下文取消可能不是最佳选择。使用取消上下文的场景是你实际上确实要取消某项操作,而不仅仅是通知下游进程发生了错误。 还需要记住的另一件事是,应该将相同的上下文实例传递给你可能要取消的所有函数和 goroutine
。
用 WithTimeout
或 WithCancel
包装一个已经支持取消功能的上下文将会造成多种可能会导致你的上下文被取消的情况,应该避免这种二次包装。
欢迎关注我们的微信公众号,每天学习 Go 知识
以上所述就是小编给大家介绍的《学会使用context取消goroutine执行的方法》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 学会使用 context 取消 goroutine 执行的方法
- 封装 axios 取消重复请求
- Git 设置和取消代理
- C# 代码取消 TFS 管控
- swift,alamofire取消之前的请求
- c – 强制允许取消引用NULL指针
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。