1.简介
本文将介绍首先为什么需要主动关闭goroutine
,并介绍如何在Go语言中关闭goroutine
的常见套路,包括传递终止信号和协程内部捕捉终止信号。之后,文章列举了需要主动关闭协程运行的常见场景,如启动一个协程执行一个不断重复的任务。希望通过本文的介绍,读者能够掌握如何在适当的时候关闭goroutine
,以及了解关闭goroutine
的常见套路。
2.为什么需要关闭goroutine
2.1 协程的生命周期
了解协程的生命周期是优雅地关闭协程的前提,因为在关闭协程之前需要知道协程的当前状态,以便采取相应的措施。所以这里我们需要先了解下goroutine
的生命周期。
在 Go
语言中,协程(goroutine)是一种轻量级的线程,可以在一个程序中同时运行多个协程,提高程序的并发性能。协程的生命周期包括创建、运行和结束三个阶段。
首先需要创建一个协程,协程的创建可以通过关键字 go 来实现,例如:
go func() {
// 协程执行的代码
}()
上面的代码会启动一个新的协程,同时在新的协程中执行匿名函数,此时协程便已被创建了。
一旦协程被创建,它就会在新的线程中运行。协程的运行状态可以由 Go 运行时(goroutine scheduler)来管理,它会自动将协程调度到适当的P
中运行,并确保协程的公平调度和平衡负载。
在运行阶段,协程会不断地执行任务,直到任务完成或者遇到终止条件。在终止阶段,协程将会被回收,从而完成其整个生命周期。
综上所述,协程由go
关键字启动,在协程中执行其业务逻辑,直到最后遇到终止条件,此时代表着协程的任务已经结束了,将进入终止阶段。最终协程将会被回收。
2.2 协程的终止条件
正常来说,都是协程任务执行完成之后,此时协程自动退出,例如:
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 协程执行的代码
fmt.Println("协程执行完毕")
}()
wg.Wait()
// 等待协程执行完毕
fmt.Println("主程序结束")
上面的代码中,我们使用 WaitGroup
等待协程执行完毕。在协程执行完毕后,程序会输出协程执行完毕和主程序结束两条信息。
还有一种情况是协程发生panic,它将会自动退出。例如:
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 协程执行的代码
panic("协程发生错误")
}()
// 等待协程执行完毕
wg.Wait()
fmt.Println("主程序结束")
}
在这种情况下,协程也会自动退出,不会再占用系统资源。
综合看来,协程的终止条件,其实就是协程中的任务执行完成了,或者是执行过程中发生了panic,协程将满足终止条件,退出执行。
2.3 为什么需要主动关闭goroutine
从上面协程的终止条件来看,正常情况下,协程只要将任务正常处理完成,协程自动退出,此时并不需要主动关闭goroutine
。
这里先举一个生产者消费者的例子,在这个例子中,我们创建了一个生产者和一个消费者,它们之间通过一个channel
进行通信。生产者生产数据并发送到一个channel
中,消费者从这个channel
中读取数据并进行处理。代码示例如下:
func main() {
// 生产者代码
go func(out chan<- int) {
for i := 0; ; i++ {
select {
case out <- i:
fmt.Printf("producer: produced %d\n", i)
time.Sleep(time.Second)
}
}
// 消费者逻辑
go func(in <-chan int) {
for {
select {
case i := <-in:
fmt.Printf("consumer: consumed %d\n", i)
}
}
}
// 让生产者协程和消费者协程一直执行下去
time.Sleep(100000000)
}
在这个例子中,我们使用了两个goroutine
:生产者和消费者。生产者向channel
中生产数据,消费者从channel
中消费数据。
但是,假如生产者出现了问题,此时生产者的协程将会被退出,不再执行。而消费者仍然在等待数据的输入。此时消费者协程已经没有存在的必要了,其实是需要退出执行。
因此,对于一些虽然没有达到终止条件的协程,但是其又没有再继续执行下去的必要,此时主动关闭其执行,从而保证程序的健壮性和性能。
3.如何优雅得关闭goroutine
优雅得关闭goroutine
的执行,我们可以遵循以下三个步骤。首先是传递关闭协程的信号,其次是协程内部需要能够到关闭信号,最后是协程退出时,能够正确释放其所占据的资源。通过以上步骤,可以保在需要时优雅地停止goroutine
的执行。下面对这三个步骤详细进行讲解。
3.1 传递关闭终止信号
首先是通过给goroutine
传递关闭协程的信号,从而让协程进行退出操作。这里可以使用context.Context
来传递信号,具体实现可以通过调用WithCancel
,WithDeadline
,WithTimeout
等方法来创建一个带有取消功能的Context
,并在需要关闭协程时调用Cancel
方法来向Context
发送取消信号。示例代码如下:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
// 调用cancel函数后,这里将能够收到通知
case <-ctx.Done():
return
default:
// do something
}
}
}(ctx)
// 在需要关闭协程时调用cancel方法发送取消信号
cancel()
这里,当我们想要终止协程的执行时,只需要调用可取消context
对象的Cancel
方法,协程内部将能够通过context
对象接收到终止协程执行的通知。
3.2 协程内部捕捉终止信号
协程内部也需要在取消信号传递过来时,能够正确被捕捉到,才能够正常终止流程。这里我们可以使用select
语句来监听取消信号。select
语句可以有多个case
子句,可以同时监听多个channel
,当select
语句执行时,它会一直阻塞,直到有一个case
子句可以执行。select
语句也可以包含default子句,这个子句在所有的case子句都不能执行时会被执行,通常用于防止select语句的阻塞。如下:
select {
case <-channel:
// channel有数据到来时执行的代码
default:
// 所有channel都没有数据时执行的代码
}
而context
对象的Done
方法刚好也是返回一个channel
,取消信号便是通过该channel
来进行传递的。所以我们可以在协程内部,通过select
语句,在其中一个case
分支来监听取消信号;同时