设计思路
- 信号处理:使用Go语言内置的
os/signal
包来捕获系统退出信号(如SIGINT
、SIGTERM
)。这使得程序能够在接收到这些信号时,启动关闭流程。
- 上下文(Context):使用
context.Context
来控制Goroutine的生命周期。context.Context
可以携带截止时间、取消信号等信息,传递给各个Goroutine,让它们能够感知到系统关闭的信号并及时退出。
- WaitGroup:使用
sync.WaitGroup
来等待所有Goroutine完成任务。每个Goroutine在启动时向WaitGroup
添加计数,在退出时减少计数。主程序使用WaitGroup
的Wait
方法等待所有Goroutine完成。
- 数据一致性:在Goroutine之间共享数据时,使用
sync.Mutex
、sync.RWMutex
等同步原语来保证数据一致性。同时,在关闭流程中,确保所有数据的修改操作已经完成,再进行资源清理。
- 避免死锁:在设计Goroutine之间的依赖关系和同步操作时,要特别注意避免死锁。例如,确保锁的获取顺序一致,避免循环依赖等。
关键代码示例
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d received shutdown signal, cleaning up...\n", id)
// 进行资源清理操作
time.Sleep(1 * time.Second) // 模拟资源清理时间
fmt.Printf("Worker %d cleanup finished\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// 启动多个Goroutine
numWorkers := 3
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
// 捕获系统退出信号
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Println()
fmt.Println(sig)
cancel() // 发送取消信号给所有Goroutine
}()
wg.Wait() // 等待所有Goroutine完成
fmt.Println("All workers have been gracefully stopped")
}
代码说明
- worker函数:每个Goroutine执行
worker
函数,它使用select
语句监听ctx.Done()
通道。当接收到取消信号时,进行资源清理并返回。
- main函数:
- 使用
context.WithCancel
创建一个可取消的上下文ctx
和取消函数cancel
。
- 启动多个Goroutine,并为每个Goroutine添加
WaitGroup
计数。
- 使用
os/signal
包捕获SIGINT
和SIGTERM
信号。当接收到信号时,调用cancel
函数取消上下文,通知所有Goroutine退出。
- 最后使用
wg.Wait()
等待所有Goroutine完成清理工作。