使用通道控制程序的生命周期

2020年12月5日 16:58

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 值,则表示任务超时。


参考资料:

  1. Go in action——William Kennedy, Brian Ketelsen, Erik St. Martin