专栏首页网管叨bi叨学会使用context取消goroutine执行的方法

学会使用context取消goroutine执行的方法

Go语言里每一个并发的执行单元叫做goroutine,当一个用Go语言编写的程序启动时,其main函数在一个单独的goroutine中运行。main函数返回时,所有的goroutine都会被直接打断,程序退出。除此之外如果想通过编程的方法让一个goroutine中断其他goroutine的执行,只能是在多个goroutine间通过context上下文对象同步取消信号的方式来实现。

这篇文章将介绍一些使用context对象同步信号,取消goroutine执行的常用模式和最佳实践,从而让我们能构建更迅捷、健壮的应用程序。如果对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

WithTimeoutWithCancel包装一个已经支持取消功能的上下文将会造成多种可能会导致你的上下文被取消的情况,应该避免这种二次包装。

本文分享自微信公众号 - 网管叨bi叨(kevin_tech),作者:KevinYan11

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入理解Golang之Context

    这篇文章将介绍Golang并发编程中常用到一种编程模式:context。本文将从为什么需要context出发,深入了解context的实现原理,以及了解如何使用...

    KevinYan
  • Fuchsia 是什么?Fuchsia OS 的未来如何?

    今天意外地看到 Fuchsia 系统即将进入 “dogfood” 测试,也就是开发者已经对系统开始亲自测试的阶段,未来不远就可能进入公开测试阶段了,这对于 Fl...

    KevinYan
  • JS module的导出和导入

    最近看了些Vue框架写的程序,发现自己的前端知识还停留在几年以前,发现现在Javascript程序里有各种各样的对module的导入和到处,导入乍一看跟pyth...

    KevinYan
  • 移动端效果之IndexList

    写在前面 接着前面的移动端效果讲,这次讲解的的是IndexList的实现原理。效果如下: ? 代码请看这里:github 移动端效果之swiper 移动端效果之...

    糊糊糊糊糊了
  • AS3关于事件流

    addEventListener的第三个参数如果为false(默认为flase), 回调事件会在冒泡状态的时候执行 ;否则会在目标状态执行。当参数为true时 ...

    py3study
  • 承衣钵者,西湖大学

    48岁出任副校长时,大概不会想到三年后,自己离开清华,做了“前无古人”的西湖大学的校长。

    镁客网
  • 啤酒和尿布放在一起卖得更好?来看看这个故事背后的Apriori算法

    Apriori算法号称是十大数据挖掘算法之一,在大数据时代威风无两,哪怕是没有听说过这个算法的人,对于那个著名的啤酒与尿布的故事也耳熟能详。但遗憾的是,随着时代...

    TechFlow-承志
  • Linux服务器文本编辑(vi和nano)介绍

    刺_猬
  • 科研“纪委”Pubpeer再揭论文造假,《Nature》昨日已撤稿

    撤回了18年9月5日发表于《Nature》上的由埃及儿童肿瘤医院及贝勒医学院等单位共同完成的有关脑瘤与T细胞的研究:A homing system target...

    用户6317549
  • 电影知识图谱问答(二)|生成298万条RDF三元组数据

    本篇文章接《电影知识图谱问答(一)|爬取豆瓣电影与书籍详细信息》,学习如何利用爬取的数据,构建知识图谱所需的三元组。主要内容包括如何从Json类型的数据,转换成...

    小一

扫码关注云+社区

领取腾讯云代金券