Go 程序取消子 Goroutine 的几种方式
5月 21, 2020
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
WithDeadline
和 WithTimeout
类似,只是时间描述维度不同,就不再介绍了。
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 了,可以根据不同的场景来选择使用。