Go语言 并发模式 --Runner


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)
   }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题;Go语言作为一个出道以来就自带...
    驻马听雪阅读 2,957评论 3 27
  • Go入门 Go介绍 部落图鉴之Go:爹好还这么努力? 环境配置 安装 下载源码编译安装 下载相应平台的安装包安装 ...
    齐天大圣李圣杰阅读 4,561评论 0 26
  • 1.春 给人希望的季节 用一身柔媚的娇嫩 唤醒芳心,赢取岁月的荣耀 就像我们初遇,怯生而又新奇 彼此来自世界各地,...
    清水无痕阅读 636评论 3 4
  • 1.我需要一个超现实的理由---精神的力量 2.每天作出自己的选择---选择的力量 3.慎重地选择朋友---关系的...
    ElinaLuo阅读 183评论 0 0