go context之WithCancel、使用context取消goroutine执行

使用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还会返回一个支持取消功能的上下文对象)。该函数不接受参数也不返回任何内容,当需要取消上下文时会调用该函数,发出取消事件。

  1. WithCancel()函数接受一个 Context 并返回其子Context和取消函数cancel
  2. 新创建协程中传入子Context做参数,且需监控子Context的Done通道,若收到消息,则退出
  3. 需要新协程结束时,在外面调用 cancel 函数,即会往子Context的Done通道发送消息
  4. 注意:当 父Context的 Done() 关闭的时候,子 ctx 的 Done() 也会被关闭

实验步骤

  1. 利用根Context创建一个父Context,使用父Context创建一个协程,

  2. 利用上面的父Context再创建一个子Context,使用该子Context创建一个协程

  3. 一段时间后,调用父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)
		}
	}
}

猜你喜欢

转载自blog.csdn.net/inthat/article/details/117435198