Go 程序取消子 Goroutine 的几种方式

Go 程序取消子 Goroutine 的几种方式

May 21, 2020
Go

Go 代码中如果有的 Goroutine 永远都不会退出,随着 Goroutine 的数量增长,内存泄露的风险也会变高。因此必须需要一种方法能够控制 Goroutine 的野蛮生长,在需要退出的场景下必须要主动退出。下面介绍几种方法来主动退出 Goroutine。

Context 方式 #

Context 包 #

Go 中的上下文 Context 包与 Goroutine 关系密切,上下文主要用在 Goroutine 之间的通信。Context 提供了三个方法方便操作子 Goroutine:

  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  • func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

其中 WithTimeout 相当于 WithDeadline(parent, time.Now().Add(timeout))。而这几个方法返回的 CancelFunc 的功能就是让那些使用了该 context 的 worker 停止工作,在需要的时候可以显式进行调用。

另外 Context 接口实现了几个方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error   
    Value(key interface{}) interface{}
}

其中 Done()cancel 调用后会都到一个消息,从而在 Goroutine 中能够检测到取消的通知,利用这个原理,就可以通过 Context 包来实现取消子 Goroutine 的功能了。

WithCancel 方式 #

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go work(ctx)
	time.Sleep(8 * time.Second)
	cancel()
	time.Sleep(2 * time.Second)
	fmt.Println("下班!")
}

func work(ctx context.Context) {
	for range time.Tick(time.Second) {
		select {
		case <-ctx.Done():
			fmt.Println("工作超时了,停止工作!")
			return
		default:
			fmt.Println("我爱工作,我在工作!")
		}
	}
}
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 我爱工作,我在工作!
// 工作时间超时了,停止工作!
// 下班!

WithTimeout 方式 #

WithTimeout 会在设置的时间过后自动调用 cancel

package main

import (
	"context"
	"fmt"
	"time"
)

const shortDuration = 1 * time.Millisecond

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}
//output:
//context deadline exceeded

WithDeadlineWithTimeout 类似,只是时间描述维度不同,就不再介绍了。

Channel 方式 #

其实上面已经间接利用了 ctx.Done() 来接受一个 channel 的消息,现在来介绍不用 Context 只使用 Channel 的情况下,如何实现取消 Goroutine。

Channel 特性 #

Channel 可以分为带缓冲区的和不带缓冲区的。

c1 := make(chan int)  // 不带缓冲区
c2 := make(chan int, 100)  // 带缓冲区

不带缓冲区 #

不带缓冲区的 channel 发送和接收动作是同时发生的,发送阻塞直到数据被接收,接收阻塞直到读到数据。例如 ch := make(chan int) ,如果没 goroutine 读取接收者<-ch ,那么发送者ch<- 就会一直阻塞。

带缓冲区的 #

带缓冲区的 channel 类似一个队列,当缓冲满时发送者阻塞,当缓冲空时接收者阻塞。

Channel 实现 #

for + select 轮询 #

package main

import "fmt"

func fibonacci(ch chan int, done chan bool) {
	x, y := 0, 1
	for {
		select {
		case ch <- x:
			x, y = y, x+y
		case <-done:
			fmt.Println("done")
			return
		}
	}
}

func main() {
	ch := make(chan int)
	done := make(chan bool)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-ch)
		}
		done <- true
	}()
	fibonacci(ch, done)
}

这里向 Channel 发送关闭通知,然后通过 Goroutine 里的 for 循环里的 select 轮询队列 done 来发现取消通知。

close channel #

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		close(ch)
	}()
	go func() {
		for val := range ch {
			fmt.Println(val)
		}
		fmt.Println("done")
	}()
	time.Sleep(5 * time.Second)
	fmt.Println("exit")
}

这里利用的是 Channel 被 close 后,就会跳出 for 循环的原理来实现。

总结 #

目前看就有三种方法可以取消 Goroutine 了,可以根据不同的场景来选择使用。

本文以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

上一篇:使用 Typora 来编辑你的 Hugo 博客

下一篇:Gin Web 框架中 Validate 使用总结