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

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

这篇文章将介绍一些使用context对象同步信号取消中断程序执行的经常使用模式和最佳实践,从而让咱们能构建更迅捷、健壮的应用程序。若是对context对象不太了解的同窗建议先仔细看看《Golang 并发编程之Context》了解一下基础。编程

为何须要取消功能

简单来讲,咱们须要取消功能来防止系统作一些没必要要的工做。浏览器

考虑如下常见的场景:一个HTTP服务器查询数据库并将查询到的数据做为响应返回给客户端:服务器

客户端请求

若是一切正常,时序图将以下所示:
请求处理时序图并发

可是,若是客户端在中途取消了请求会发生什么?这种状况能够发生在,好比用户在请求中途关闭了浏览器。若是不支持取消功能,HTTP服务器和数据库会继续工做,因为客户端已经关闭因此他们工做的成果也就被浪费了。这种状况的时序图以下所示:函数

不支持取消的处理时序图

理想状况下,若是咱们知道某个处理过程(在此示例中为HTTP请求)已中止,则但愿该过程的全部下游组件都中止运行:工具

支持取消的处理时序图

使用context实现取消功能

如今咱们知道了应用程序为何须要取消功能,接下来咱们开始探究在Go中如何实现它。由于“取消事件”与正在执行的操做高度相关,所以很天然地会将它与上下文捆绑在一块儿。测试

取消功能须要从两方面实现才能完成:google

  • 监听取消事件
  • 发出取消事件

监听取消事件

Go语言context标准库的Context类型提供了一个Done()方法,该方法返回一个类型为 <-chan struct{}channel。每次context收到取消事件后这个channel都会接收到一个struct{}类型的值。因此在Go语言里监听取消事件就是等待接收<-ctx.Done()spa

举例来讲,假设一个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包装一个已经支持取消功能的上下文将会形成多种可能会致使你的上下文被取消的状况,应该避免这种二次包装。

相关文章
相关标签/搜索