Golang 语言原生支持并发,一旦用到并发,就可能涉及共享资源读写的问题,除了原子函数、互斥锁等传统方式,Golang 还提供了通道,使用通道来管理并发资源的读写问题,更简单且更不容易出错。
通道
通道分为两种:无缓冲通道和有缓冲通道,前者要求两个 goroutine 使用同步的方式发送和接收资源,后者则像一个异步队列,性能更高。
一般来说,为了追求高性能,我们都会使用异步,但是通过合理搭配两种通道的特性,可以做一些有用的事情,比如控制程序的生命周期。
控制程序的生命周期
通常我们在编写一些定时任务或离线任务时,需要管理程序的生命周期:
- 程序执行前,检查系统中断信号,则需要结束程序转去处理中断
- 任务执行时间过长时,结束程序
由于系统中断可能随时产生,我们不可能一直阻塞等待该事件发生,所以可以使用带缓冲的通道接收中断信号,等待何时时机进行处理:
interrupt := make(chan os.Signal, 1)
主线程需要等待子任务的执行结果,确切的说就是结束、中断、超时这些事件,在发生这些事件前,都不应该退出,这个过程可以使用无缓冲的通道来实现同步:
complete := make(chan error)
timeout := make(chan time.Time)
其中 complete 通道接收程序出错或结束的消息,timeout 这个通道如果收到一个 time.Time 的值,这个程序就会试图清理状态并停止工作,很明显,应当在到达程序配置的超时时间时,给该通道发送消息。
作为示例,可以增加一个切片来存储需要执行的任务 tasks []func(int)
因此,整个主体结构可以这么定义:
type Runner struct {
interrunpt chan os.Signal
complete chan error
timeout <-chan time.Time
tasks []func(int)
}
// 工厂函数
func New(d time.Duration) *Runner {
return &Runner{
interrupt: make(chan os.Signal, 1), // 带缓冲的通道,接收终端信号
complete: make(chan error), // 无缓冲通道,接收程序错误
timeout: time.After(d), // 无缓冲通道,在 d 这段时间后发送 time.Time 值
}
}
编写一个 run 函数,用于执行所有注册的任务 tasks:
// 定义两种错误类型
var ErrTmeout = errors.New('received timeout')
var ErrInterrupt = errors.New('received interrupt')
func (r *Runner) run() error {
for id, task := range r.tasks {
// 检测操作系统的终端信号
if r.gotInterrupt() {
return ErrInterrupt
}
task(id)
}
return nil
}
// 检查系统中断信号
func (r *Runner) gotInterrupt() {
select {
case <- r.interrupt:
// 停止接收后续信号
signal.Stop(r.interrupt)
return true
}
default:
// 有了 default 分支就不会阻塞
return false
}
在主流程中创建 goroutine 执行任务,并在得到结果前保持阻塞
func (r *Runner) Start() error {
// 接收所有中断信号
signal.Notify(r.interrunpt, os.Interrupt)
// 启动一个 goroutine 执行任务
go func() {
r.complete <- r.run()
}()
select {
case err := <- r.complete:
return err
case <- r.timeout:
return ErrTimeout
}
}
这里使用了无 default 的 select 结构,因为没有 default 出口,所以会一直阻塞,等待两个事件中的任意一个,如果从 complete 通道接收到了 error 接口值,那么 goroutine 要么正常完成了工作,要么遇到了中断信号,如果从 timeout 通道接收到了 time.Time 值,则表示任务超时。
参考资料:
- Go in action——William Kennedy, Brian Ketelsen, Erik St. Martin