Runner
用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用 runner 包来终止程序。
在设计上,可以实现以下几点
①程序可以在分配的时间内完成工作,正常终止;
②程序没有及时完成工作,“自杀”;
③接收到操作系统发送的中断事件,程序立刻试图清理状态并停止工作。
示例代码如下:
//Runner 在给定的超时时间内执行一组任务
// 并且在操作系统发送中断信号时结束这些任务
type Runner struct {
//从操作系统发送信号
interrupt chan os.Signal
//报告处理任务已完成
complete chan error
//报告处理任务已经超时
timeout <- chan time.Time
//持有一组以索引为顺序的依次执行的以int类型id为参数的函数
tasks []func(id int)
}
//统一错误处理
//超时错误信息,会在任务执行超时时返回
var ErrTimeOut = errors.New("received timeout")
//中断错误信号,会在接收到操作系统的事件时返回
var ErrInterrupt = errors.New("received interrupt")
//New 函数返回一个新的准备使用的Runner,d:自定义分配的时间
func New(d time.Duration) *Runner{
return &Runner{
interrupt:make(chan os.Signal,1),
complete:make(chan error),
//会在另一线程经过时间段d后向返回值发送当时的时间。
timeout:time.After(d),
}
}
//将一个任务加入到Runner中
func (r *Runner)Add(tasks ...func(id int)) {
r.tasks = append(r.tasks,tasks...)
}
//开始执行所有任务,并监控通道事件
func (r *Runner)Start()error {
//监控所有的中断信号
signal.Notify(r.interrupt,os.Interrupt)
//使用不同的goroutine执行不同的任务
go func() {
r.complete <- r.run()
}()
//使用 select 语句来监控goroutine的通信
select {
//等待任务完成
case err := <- r.complete:
return err
//任务超时
case <- r.timeout:
return ErrTimeOut
}
}
//执行每一个已注册的任务
func (r *Runner)run()error {
for id,task := range r.tasks{
//检测操作系统的中断信号
if r.gotInterrupt() {
return ErrInterrupt
}
//执行已注册的任务
task(id)
}
return nil
}
//检测是否收到了中断信号
func (r *Runner)gotInterrupt()bool {
select {
//当中断事件被触发时
case <- r.interrupt:
//停止接收后续的任何信号
signal.Stop(r.interrupt)
return true
//继续执行
default:
return false
}
}
分析:
(1)在构建Runner结构体时声明了 3 个通道,用来辅助管理程序的生命周期,以及用来表示顺序执行的不同任务的函数切片。
interrupt:通道收发 os.Signal 接口类型的值,用来从主机操作系统接收中断事件。
os.Signal接口的声明:
// Signal 用来描述操作系统发送的信号。其底层实现通常会依赖操作系统的具体实现
type Signal interface {
String() string
Signal() // to distinguish from other Stringers
}
complete:是一个收发 error 接口类型值的通道。
这个通道被执行任务的 goroutine 用来发送任务已经完成的信号。如果执行任务时发生了错误,会通过这个通道发回一个 error 接口类型的值。如果没有发生错误,会通过这个通道发回一个 nil 值作为 error 接口值。
timeout:接收 time.Time 值
这个通道用来管理执行任务的时间。如果从这个通道接收到一个 time.Time 的值,这个程序就会试图清理状态并停止工作。
tasks:函数值的切片,代表一个接一个顺序执行的函数。会有一个与 main 函数分离的 goroutine 来执行这些函数。
(2)两个 error 接口变量,分别代表不同的错误值
ErrTimeout:会在收到超时事件时,由 Start方法返回。
ErrInterrupt:会在收到操作系统的中断事件时,由 Start 方法(具体是run方法)返回。
(3)创建一个 Runner 类型的值
New 函数:接收一个 time.Duration 类型的值,并返回 Runner 类型的指针。这个函数会创建一个 Runner 类型的值,并初始化每个通道字段。因为 task 字段的零值是 nil,已经满足初始化的要求,所以没有被明确初始化。
interrupt: 被初始化为缓冲区容量为 1 的通道。这可以保证通道至少能接收一个来自语言运行时的 os.Signal 值,确保语言运行时发送这个事件的时候不会被阻塞。如果 goroutine没有准备好接收这个值,这个值就会被丢弃。
complete:被初始化为无缓冲的通道。当执行任务的 goroutine 完成时,会向这个通道发送一个 error 类型的值或者 nil 值。之后就会等待 main 函数接收这个值。一旦 main 接收了这个 error 值,goroutine 就可以安全地终止了。
timeout:用 time 包的 After 函数初始化的。After 函数返回一个time.Time 类型的通道。语言运行时会在指定的 duration 时间到期之后,向这个通道发送一个 time.Time 的值。
(4)与Runner关联的方法
Add方法: 用来增加一个要执行的任务函数。接收一个名为 tasks 的可变参数。可变参数可以接受任意数量的值作为传入参数。这个例子里,这些传入的值必须是一个接收一个整数且什么都不返回的函数。函数执行时的参数 tasks 是一个存储所有这些传入函数值的切片。
run方法:迭代 tasks 切片,并按顺序执行每个函数。在执行之前,会调用 gotInterrupt 方法来检查是否有要从操作系统接收的事件。
gotInterrupt方法:使用 select 语句来监控 goroutine 的通信,会让 goroutine 检查中断信号,如果没有发出中断信号,就继续处理工作。
首先试图从 interrupt 通道去接收信号,如果 interrupt 通道有中断信号需要接收,就会接收并处理这个中断。如果没有需要接收的信号,就会执行 default 分支。当收到中断信号后,代码会通过调用 Stop 方法来停止接收之后的所有事件。之后函数返回 true。如果没有收到中断信号,会返回 false。
start方法:实现了程序的主流程。通过signal.Notify(r.interrupt, os.Interrupt)接收所有中断信号,声明了一个匿名函数,并单独启动goroutine 来执行。这个 goroutine 会执行一系列被赋予的任务。在 goroutine 的内部调用了 run 方法,并将这个方法返回的 error 接口值发送到 complete 通道。一旦 error 接口的值被接收,该 goroutine 就会通过通道将这个值返回给调用者。创建 goroutine 后,Start 进入一个 select 语句,阻塞等待两个事件中的任意一个。如果从 complete 通道接收到 error 接口值,那么该 goroutine 要么在规定的时间内完成了分配的工作,要么收到了操作系统的中断信号。无论哪种情况,收到的 error 接口值都会被返回,随后方法终止。如果从 timeout 通道接收到 time.Time 值,就表示 goroutine 没有在规定的时间内完成工作。这种情况下,程序会返回 ErrTimeout 变量。
main.go演示Runner测试
// timeout 规定了必须在多少秒内处理完成
const timeout = 3 * time.Second
func main() {
log.Println("Starting work.")
// 为本次执行分配超时时间
r := New(timeout)
// 加入要执行的任务
r.Add(createTask(), createTask(), createTask())
// 执行任务并处理结果
if err := r.Start(); err != nil {
switch err {
case ErrTimeOut:
log.Println("Terminating due to timeout.")
os.Exit(1)
case ErrInterrupt:
log.Println("Terminating due to interrupt.")
os.Exit(2)
}
}
log.Println("Process ended.")
}
//createTask 返回一个根据 id 休眠指定秒数的示例任务
func createTask() func(int) {
return func(id int) {
log.Printf("Processor - Task #%d.", id)
time.Sleep(time.Duration(id) * time.Second)
}
}