Golang语言情怀-第40期 Go 语言设计模式 信号

2021-03-09 10:55:00 浏览数 (1)

1、使用场景

实际项目中,我们希望修改了配置文件后,但又不想通过重启进程让它重新加载配置文件,可以使用signal的方式进行信号传递,或者我们希望通过信号控制,实现一种优雅的退出方式。Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。

2、常用的Term信号

3、简单的栗子

代码语言:javascript复制
package main

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

func main() {
        c := make(chan os.Signal)
        signal.Notify(c)
        fmt.Println("start..")
        s := <-c
        fmt.Println("End...", s)
}

(1)传递SIGINT信号

代码语言:javascript复制
[homework@xxxxx signal]$ go run monitor.go
start..




#此时,CTL C发送一个SIGINT信号量,得到输出为:
[homework@xxxxx signal]$ go run monitor.go
start..
^CEnd... interrupt

(2)传递SIGTERM信号

打开2个Term窗口 第一个运行go run monitor.go程序 第二个执行:ps -ef | grep monitor.go | grep grep -v | awk '{print $2}' | xargs kill

代码语言:javascript复制
#此时,kill命令发送一个SIGTERM信号量,得到输出为:
[homework@xxxxx signal]$ go run monitor.go
start..
Terminated

4、优雅的退出守护进程

(1)何为优雅(graceful)?

Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态。

在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,即希望进程优雅退出。

(2)从对优雅退出的理解不难看出:优雅退出可以通过捕获SIGTERM来实现。

A、注册SIGTERM信号的处理函数并在处理函数中做一些进程退出的准备,信号处理函数的注册sigaction()来实现。

B、在主进程的main()中,通过类似于while(!fQuit)的逻辑来检测那个flag变量,一旦fQuit在signal handler function中被置为true,则主进程退出while()循环,接下来就是一些释放资源或dump进程当前状态或记录日志的动作,完成这些后,主进程退出。

栗子:优雅退出go守护进程

代码语言:javascript复制
package main

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

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("Program Exit...", s)
                                GracefullExit()
                        case syscall.SIGUSR1:
                                fmt.Println("usr1 signal", s)
                        case syscall.SIGUSR2:
                                fmt.Println("usr2 signal", s)
                        default:
                                fmt.Println("other signal", s)
                        }
                }
        }()

        fmt.Println("Program Start...")
        sum := 0
        for {
                sum  
                fmt.Println("sum:", sum)
                time.Sleep(time.Second)
        }
}

func GracefullExit() {
        fmt.Println("Start Exit...")
        fmt.Println("Execute Clean...")
        fmt.Println("End Exit...")
        os.Exit(0)
}

执行程序:

代码语言:javascript复制
[homework@xxxx signal]$ go run monitor.go
Program Start...
sum: 1
sum: 2
sum: 3
sum: 4
sum: 5
sum: 6
^CProgram Exit... interrupt
Start Exit...
Execute Clean...
End Exit...

5、信号的订阅

信号的订阅是通过 channel实现的,每个os.Signal channel 都会收听自己相应的事件集。

关于Golang之信号处理的文章就写到这里,Done!

信号量

2.1 共享变量

在理解信号量之前,先了解采用共享变量使用多线程会出现什么问题。下面是一个C代码片段

代码语言:javascript复制
1for (i=0; i<niters; i  ){
2    cnt   ;
3}

cnt为全局变量,一个线程执行该代码片段的时候的汇编代码如下:

代码语言:javascript复制
 1   movq (%rdi), %rcx
 2    testq %rcx, %rcx
 3    jle .L2
 4    movl $0, �x
 5.L3:
 6    movq cnt(%rip), %rdx
 7    addq �x
 8    movq �x, cnt(%rip)
 9    addq $1, %rax
10    cmpq %rcx, %rax
11    jne .L3
12.L2

2.2 信号量其中6-8行分别对应对应着加载cnt,更新cnt和存储cnt。将cnt变量从内存位置读出,加载到CPU寄存器中,在CPU运算器中加1,然后存储到cnt的内存位置。虽然代码中cnt 只有一行,但是转换为汇编代码的时候不只有一个操作,也就是说该语句不是原子操作。如果多个线程同时执行代码,按照之前的条件,不对CPU的执行顺序做任何假设,如果其中线程a在执行7行汇编代码,而线程b执行6行汇编代码,那么b将"看不到"线程a对全局变量cnt加1的操作,那么每次执行的结果cnt也不完全一致。

计算机领域先驱Dijkstra提出经典的解决上述问题的方法:信号量(semaphore)。它是一个非负整数的全局变量。而且该变量只能有两个特殊操作来处理: PV

  • P(s):如果s非零,那么Ps1,并且立即返回。如果s为零,那么就挂起这个线程,知道s为非零。
  • V(s): V操作将s1。如果有任何线程阻塞在P操作等待s非零,那么V将重启其中线程中的一个。

Posix标准定义需要操作信号量的函数

代码语言:javascript复制
1#include <semaphore.h>
2int sem_init(sem_t *sem, 0, unsigned int value);
3int sem_wait(sem_t *s); /*P(s)*/
4int sem_post(sem_t *s); /*P(s)*/

那么如何使用信号量是的2.1小节出现同步问题解决呢?首先定义全局信号量

代码语言:javascript复制
1volatile long cnt = 0; /* global variable */
2sem_t mutex; /*global semaphore*/

初始化信号量,在这里初始值为1

代码语言:javascript复制
1sem_init(&mutex, 0, 1);

最后使用信号量操作函数将临界区域代码包含起来

代码语言:javascript复制
1for (i =0; i<niters; i  ){
2    sem_wait(&mutex);
3    cnt  ;
4    sem_post(&mutex);
5}

参考资料:

Go语言信号处理

https://zhuanlan.zhihu.com/p/128953024

信号量,锁和 golang 相关源码分析

https://cloud.tencent.com/developer/article/1357636

0 人点赞