Golang 学习笔记十一 os/signal包 和 实例runner

一、os/signal包实现对信号的处理

参考
golang中os/signal包的使用
Golang信号处理和优雅退出守护进程

golang中对信号的处理主要使用os/signal包中的两个方法:一个是notify方法用来监听收到的信号;一个是 stop方法用来取消监听。

1.Notify
func Notify(c chan<- os.Signal, sig ...os.Signal)

第一个参数表示接收信号的channel, 第二个及后面的参数表示设置要监听的信号,如果不设置表示监听所有的信号。

func main() {
    c := make(chan os.Signal, 0)
    signal.Notify(c)
 
    // Block until a signal is received.
    s := <-c
    fmt.Println("Got signal:", s) //Got signal: terminated
}

结果分析:运行该程序,然后在终端中通过kill命令杀死对应的进程,便会得到结果

2.Stop
func Stop(c chan<- os.Signal)
func main() {
    c := make(chan os.Signal, 0)
    signal.Notify(c)
    
    //不允许继续往c中存入内容
    signal.Stop(c)
    //c无内容,此处阻塞,所以不会执行下面的语句,也就没有输出
    s := <-c
    fmt.Println("Got signal:", s)
}

3.优雅退出go守护进程
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// 优雅退出go守护进程
func main()  {
    //创建监听退出chan
    c := make(chan os.Signal)
    //监听指定信号 ctrl+c kill
    signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, 
    syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
    go func() {
        for s := range c {
            switch s {
            case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
                fmt.Println("退出", s)
                ExitFunc()
            case syscall.SIGUSR1:
                fmt.Println("usr1", s)
            case syscall.SIGUSR2:
                fmt.Println("usr2", s)
            default:
                fmt.Println("other", s)
            }
        }
    }()

    fmt.Println("进程启动...")
    sum := 0
    for {
        sum++
        fmt.Println("sum:", sum)
        time.Sleep(time.Second)
    }
}

func ExitFunc()  {
    fmt.Println("开始退出...")
    fmt.Println("执行清理...")
    fmt.Println("结束退出...")
    os.Exit(0)
}

-----------------------------
kill -USR1 pid 输出
usr1 user defined signal 1

kill -USR2 pid 
usr2 user defined signal 2

kill pid 
退出 terminated
开始退出...
执行清理...
结束退出...

执行输出
go run example-3.go
进程启动...
sum: 1
sum: 2
sum: 3
sum: 4
sum: 5
sum: 6
sum: 7
sum: 8
sum: 9
usr1 user defined signal 1
sum: 10
sum: 11
sum: 12
sum: 13
sum: 14
usr2 user defined signal 2
sum: 15
sum: 16
sum: 17
退出 terminated
开始退出...
执行清理...
结束退出...

二、Go语言实战P166第7章 实例runner

runner 包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用 runner 包来终止程序。当开发需要调度后台处理任务的程序的时候,这种模式会很有用。这个程序可能会作为 cron 作业执行,或者在基于定时任务的云环境(如 iron.io)里执行。


//main.go
// 这个示例程序演示如何使用通道来监视
// 程序运行的时间,以在程序运行时间过长
// 时如何终止程序
package main

import (
    "log"
    "os"
    "time"

    "github.com/goinaction/code/chapter7/patterns/runner"
)

// timeout 规定了必须在多少秒内处理完成
const timeout = 3 * time.Second

// main 是程序的入口
func main() {
    log.Println("Starting work.")

    // 为本次执行分配超时时间
    r := runner.New(timeout)

    // 加入要执行的任务
    r.Add(createTask(), createTask(), createTask())

    // 执行任务并处理结果
    if err := r.Start(); err != nil {
        switch err {
        case runner.ErrTimeout:
            log.Println("Terminating due to timeout.")
            os.Exit(1)
        case runner.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)
    }
}


//runner.go
// Gabriel Aszalos 协助完成了这个示例
// runner 包管理处理任务的运行和生命周期
package runner

import (
    "errors"
    "os"
    "os/signal"
    "time"
)

// Runner 在给定的超时时间内执行一组任务,
// 并且在操作系统发送中断信号时结束这些任务
type Runner struct {
    // interrupt 通道报告从操作系统
    // 发送的信号
    interrupt chan os.Signal

    // complete 通道报告处理任务已经完成
    complete chan error

    // timeout 报告处理任务已经超时
    timeout <-chan time.Time

    // tasks 持有一组以索引顺序依次执行的
    // 函数
    tasks []func(int)
}

// ErrTimeout 会在任务执行超时时返回
var ErrTimeout = errors.New("received timeout")

// ErrInterrupt 会在接收到操作系统的事件时返回
var ErrInterrupt = errors.New("received interrupt")

// New 返回一个新的准备使用的 Runner
// task 字段的零值是 nil,已经满足初始化的要求
// ,所以没有被明确初始化
func New(d time.Duration) *Runner {
    return &Runner{
        interrupt: make(chan os.Signal, 1),
        complete:  make(chan error),
        timeout:   time.After(d),
    }
}

// Add 将一个任务附加到 Runner 上。这个任务是一个
// 接收一个 int 类型的 ID 作为参数的函数
func (r *Runner) Add(tasks ...func(int)) {
    r.tasks = append(r.tasks, tasks...)
}

// Start 执行所有任务,并监视通道事件
func (r *Runner) Start() error {
    // 我们希望接收所有中断信号
    signal.Notify(r.interrupt, os.Interrupt)

    // 用不同的 goroutine 执行不同的任务
    go func() {
        r.complete <- r.run()
    }()

    select {
    // 当任务处理完成时发出的信号
    case err := <-r.complete:
        return err

    // 当任务处理程序运行超时时发出的信号
    case <-r.timeout:
        return ErrTimeout
    }
}

// run 执行每一个已注册的任务
func (r *Runner) run() error {
    for id, task := range r.tasks {
        // 检测操作系统的中断信号
        if r.gotInterrupt() {
            return ErrInterrupt
        }

        // 执行已注册的任务
        task(id)
    }

    return nil
}

// gotInterrupt 验证是否接收到了中断信号
func (r *Runner) gotInterrupt() bool {
    select {
    // 当中断事件被触发时发出的信号
    case <-r.interrupt:
        // 停止接收后续的任何信号
        signal.Stop(r.interrupt)
        return true

    // 继续正常运行
    default:
        return false
    }
}

1.通道 interrupt 被初始化为缓冲区容量为 1 的通道。这可以保证通道至少能接收一个来自语言运行时的 os.Signal 值,确保语言运行时发送这个事件的时候不会被阻塞。如果 goroutine没有准备好接收这个值,这个值就会被丢弃。例如,如果用户反复敲 Ctrl+C 组合键,程序只会在这个通道的缓冲区可用的时候接收事件,其余的所有事件都会被丢弃。

2.通道 complete 被初始化为无缓冲的通道。当执行任务的 goroutine 完成时,会向这个通道发送一个 error 类型的值或者 nil 值。之后就会等待 main 函数接收这个值。一旦 main 接收了这个 error 值,goroutine 就可以安全地终止了。

3.最后一个通道 timeout 是用 time 包的 After 函数初始化的。After 函数返回一个time.Time 类型的通道。语言运行时会在指定的 duration 时间到期之后,向这个通道发送一个 time.Time 的值。

4.select default

87 // gotInterrupt 验证是否接收到了中断信号
88 func (r *Runner) gotInterrupt() bool {
89  select {
90  // 当中断事件被触发时发出的信号
91  case <-r.interrupt:
92  // 停止接收后续的任何信号
93  signal.Stop(r.interrupt)
95  return true
96
97  // 继续正常运行
98  default:
99  return false
100  }
101 }

在第 91 行,代码试图从 interrupt 通道去接收信号。一般来说,select 语句在没有任何要接收的数据时会阻塞,不过有了第 98 行的 default 分支就不会阻塞了。default 分支会将接收 interrupt 通道的阻塞调用转变为非阻塞的。如果 interrupt 通道有中断信号需要接收,就会接收并处理这个中断。如果没有需要接收的信号,就会执行 default 分支。当收到中断信号后,代码会通过在第 93 行调用 Stop 方法来停止接收之后的所有事件。之后函数返回 true。如果没有收到中断信号,在第 99 行该方法会返回 false。本质上,gotInterrupt 方法会让 goroutine 检查中断信号,如果没有发出中断信号,就继续处理工作。

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

推荐阅读更多精彩内容