协程在正常情况下的关闭机制
对于一个正在运行的协程,正常在以下2种情况下会关闭。
- 协程运行完了,会自动关闭。
- 协程种发生panic时,会自动关闭。此时整个程序也会停止运行。
为什么需要关闭协程
对于一个正在运行的协程,其还未达到终止条件,但是其又没有再继续执行下去的必要,此时就需要主动关闭其执行,从而保证程序的健壮性和性能。
如何优雅的关闭协程
优雅得关闭goroutine的执行,我们可以遵循以下三个步骤。
1. 传递终止信号
首先是通过给goroutine传递关闭协程的信号,从而让协程进行退出操作。这里可以使用 context.Contex t来传递信号,具体实现可以通过调用 WithCancel 等方法来创建一个带有取消功能的Context,并在需要关闭协程时调用 Cancel 方法来向 Context 发送取消信号。示例代码如下:
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
// 调用cancel函数后,这里将能够收到通知,就能执行return
case <-ctx.Done():
return
default:
// do something
}
}
}(ctx)
// 在需要关闭协程时调用cancel方法发送取消信号
cancel()
2. 协程内部捕捉终止信号
协程内部也需要在取消信号传递过来时,将其成功捕捉到,才能够正常终止流程。这里我们可以使用select语句来监听取消信号。context对象的Done方法刚好也是返回一个channel,取消信号便是通过该channel来进行传递的
go func(ctx context.Context) {
for {
select {
// 调用cancel函数后,这里将能够收到通知,并退出协程
case <-ctx.Done():
return
default:
// 执行业务逻辑
}
}
}(ctx)
3. 回收协程资源
在垃圾回收方面,Go语言采用的是自动垃圾回收(Garbage Collection,
GC)机制,它会定期扫描和回收不再被引用的内存对象。但是,GC只针对堆上的内存进行管理,对于其他类型的资源,如栈上的内存、文件句柄、数据库连接等,则需要程序员自己进行管理和释放。
当协程被终止执行时,其占用的资源,包括文件句柄、内存等,不会根据GC机制自动管理释放,需要我们主动管理释放,以便其他程序可以继续使用这些资源。这里我们使用defer语句来确保协程在退出时能够正确地释放资源。比如协程中打开了一个文件,此时可以通过defer语句来关闭,避免资源的泄漏。
func doWork() {
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Do some work
}
不关闭会有什么风险
1. 资源泄漏
当协程没有被正确关闭时,它可能会一直保持运行状态,并一直占用各种资源,如内存、CPU、网络连接、文件句柄等,影响程序整体性能和稳定性。
2. 内存泄漏
每个协程都有自己的栈空间,如果协程没有被正确关闭,其栈上分配的内存空间也无法被回收。这会导致程序内存消耗不断增加,直到耗尽系统内存。
3. 死锁和竞争条件
如果协程之间存在互斥锁(sync.Mutex)或其他同步原语的使用,而其中一个协程没有被正确关闭,就可能会导致死锁的发生。例子如下
```go
func main() {
var wg sync.WaitGroup
wg.Add(2)
// 创建两个互斥锁
mutex1 := &sync.Mutex{}
mutex2 := &sync.Mutex{}
// 启动两个协程
go func() {
defer wg.Done()
// 获取mutex1锁
mutex1.Lock()
fmt.Println("协程1获取到mutex1锁")
// 试图获取mutex2锁,但此时mutex2已被协程2持有
mutex2.Lock()
fmt.Println("协程1获取到mutex2锁")
// 释放锁
mutex2.Unlock()
mutex1.Unlock()
}()
go func() {
defer wg.Done()
// 获取mutex2锁
mutex2.Lock()
fmt.Println("协程2获取到mutex2锁")
// 试图获取mutex1锁,但此时mutex1已被协程1持有
mutex1.Lock()
fmt.Println("协程2获取到mutex1锁")
// 释放锁
mutex1.Unlock()
mutex2.Unlock()
}()
wg.Wait()
fmt.Println("程序结束")
}
协程1获取mutex1锁并输出"协程1获取到mutex1锁"。
协程2获取mutex2锁并输出"协程2获取到mutex2锁"。
协程1尝试获取mutex2锁,但此时mutex2已被协程2持有,所以协程1被阻塞。
协程2尝试获取mutex1锁,但此时mutex1已被协程1持有,所以协程2也被阻塞。
由于两个协程相互等待对方释放锁,程序进入死锁状态,无法继续执行。
本文参考原文:https://juejin.cn/post/7232824323751936055#heading-6