Go 退出通知机制
写这篇文章灵来源自 并发的退出 - Go语言圣经。
所以本文大概是个学习笔记。
Go语言并没有提供在一个 goroutine 中终止另一个 goroutine 的方法,由于这样会导致 goroutine 之间的共享变量落在未定义的状态上。
在 Go 语言中,我们通常使用并发编程来实现高效的并发处理。在这种情况下,我们通常需要使用一些机制来协调不同 goroutine 之间的工作。其中一种常见的机制是退出通知机制,也称为取消机制。在本文中,我们将介绍什么是退出通知机制,以及如何在 Go 中实现它。
什么是退出通知机制
退出通知机制是一种在多个 goroutine 之间进行同步和通信的常见方式。它通常用于在程序的某个地方关闭一个通道(也称为退出通道或取消通道),以通知其他 goroutine 停止运行。然后,在每个 goroutine 中,我们可以使用一个函数来检查该通道是否已经被关闭,以决定是否需要终止当前的 goroutine。
在 Go 语言中,我们通常使用 select 语句来监听多个通道,以实现退出通知机制。具体来说,我们可以定义一个名为 done 的通道,并在程序的某个地方使用 close(done) 来关闭该通道。然后,在每个 goroutine 中,我们可以使用一个函数来检查 done 通道是否已经被关闭,以决定是否需要终止当前的 goroutine。这个函数通常被称为 cancelled(),具体实现如下:
func cancelled() bool {
select {
case <-done:
return true
default:
return false
}
}
在这个函数中,我们使用了一个 select 语句来监听 done 通道。如果 done 通道已经被关闭,那么 case <-done: 分支会被选择,并返回 true。否则,default: 分支会被选择,并返回 false。
补充:为什么会这么选择
1. select 语句的执行原理大致如下:
- 首先,
select语句会创建一个空的通信选择器。 - 然后,
select语句会依次检查每个case分支,从中选择一个可执行的分支。如果多个分支同时可执行,则会随机选择一个分支。 - 如果找到了一个可执行的分支,则执行该分支中的语句,并退出
select语句。如果所有分支都不可执行,则进入阻塞状态,等待其中一个分支变为可执行。 - 当一个或多个分支变为可执行时,
select语句会重新检查所有分支,并选择其中一个可执行的分支。如果多个分支同时可执行,则会随机选择一个分支,并执行该分支中的语句。 - 重复执行步骤 4,直到
select语句退出或被中断。
2. 为什么 done 通道关闭后会选择 case: <-done 分支:
如果某个通道已经被关闭,那么该通道会一直处于可读状态,即每次读取该通道都会返回其所传输的零值。即当一个通道被关闭时,该通道的读取操作会立即返回该通道的零值,而不会阻塞等待数据的到来。
具体来说,当 done 通道被关闭时,select 语句会立即 done 通道中读取一个零值,并选择该通道的 case 分支执行。
3. 为什么通道没有关闭时不选择 case: <-done 分支:
在代码中使用 select 语句监听一个未关闭的通道时,如果该通道没有数据可读,则会进入阻塞状态,等待该通道有数据可读或该通道被关闭。
为什么使用退出通知机制
退出通知机制是一种在多个 goroutine 之间进行同步和通信的常见方式,它可以帮助我们实现以下目标:
- 安全退出:我们可以使用退出通知机制来安全地停止正在运行的 goroutine,以避免死锁或其他意外情况的发生。
- 资源释放:我们可以使用退出通知机制来释放正在使用的资源,以避免内存泄漏或其他资源泄漏的发生。
- 防止僵尸 goroutine:如果一个 goroutine 在完成工作后不及时退出,那么它将成为一个“僵尸 goroutine”,占用系统资源并可能导致其他问题的发生。使用退出通知机制,我们可以及时停止这些“僵尸 goroutine”。
如何实现退出通知机制
在 Go 语言中,实现退出通知机制可以分为以下几个步骤:
- 定义一个退出通道:我们可以定义一个名为
done的通道,并使用close(done)来关闭该通道,以通知其他 goroutine 停止运行。 - 在每个 goroutine 中使用
select语句监听done通道:我们可以在每个 goroutine 中使用一个函数来检查done通道是否已经被关闭,以决定是否需要终止当前的 goroutine。这个函数通常被称为cancelled(),具体实现可以参考前面的代码示例。 - 在程序的适当位置关闭
done通道:我们需要在程序的适当位置使用close(done)来关闭done通道,以通知所有监听该通道的 goroutine 停止运行。
示例代码
package main
import (
"fmt"
"sync"
"time"
)
func cancelled(done <-chan struct{}) bool {
select {
case <-done:
return true
default:
return false
}
}
// 定义一个名为 worker 的函数,接收三个参数:
// id 表示当前 goroutine 的编号
// wg 是一个 *sync.WaitGroup 类型的指针,用于等待所有 goroutine 完成
// stopCh 是一个 <-chan struct{} 类型的通道,用于接收退出通知
func worker(id int, wg *sync.WaitGroup, done <-chan struct{}) {
// 在函数结束时调用 wg.Done(),标记一个 goroutine 已经完成
defer wg.Done()
// 在函数开始时打印一条消息,指示该 goroutine 已经开始
fmt.Printf("worker %d: started\n", id)
// 在函数结束时打印一条消息,指示该 goroutine 已经退出
defer fmt.Printf("worker %d: exited\n", id)
// 在取消事件之后创建的 goroutine 改变为无操作
if cancelled(done) {
return
}
// 使用一个无限循环来模拟一些工作
for {
select {
// 监听 done 通道,如果通道被关闭,则立即返回
case <-done:
return
default:
fmt.Printf("worker %d: working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 创建一个 done 通道,用于发送退出通知
done := make(chan struct{})
// 创建一个 sync.WaitGroup 类型的变量 wg,用于等待所有 goroutine 完成
var wg sync.WaitGroup
// 启动 5 个 goroutine,并在后台运行 worker 函数
for i := 0; i < 5; i++ {
// 在循环中,每个 goroutine 都需要增加 wg 的计数器
wg.Add(1)
go worker(i, &wg, done)
}
// 模拟一些工作,让程序运行一段时间
time.Sleep(2 * time.Second)
// 关闭 done 通道,向所有 goroutine 发送退出通知
close(done)
// 启动一个新的 goroutine 运行 worker 函数
wg.Add(1)
go worker(5, &wg, done)
// 等待所有 goroutine 完成
wg.Wait()
fmt.Println("all workers stopped")
}