python模块 signal 的使用(信号捕捉,gevent使用)

在python程序中,通常拦截的信号有两种,一个是发出的kill信号,一个是发出的ctrl+c信号

signal.signal(signal.SIGTERM, self._term_handler)   # SIGTERM 关闭程序信号
signal.signal(signal.SIGINT, self._term_handler)  # 接收ctrl+c 信号

def _term_handler(self, signal_num, frame):  
     # 信号处理函数需要2个参数,这里放在了类里面,所以还需要额外的self参数
      logger.info("Get TERM signal {0}".format(signal_num))
      self.terminated_flag = True
      self._kill_sleep_gevent()   # 轮询结束休眠的协程

设置信号拦截的目的

如果你用了多线程或者多协程,那么为了防止主线程结束了而子线程和子协程还在运行,这个时候就需要
使用signal模块了,使用signal模块可以为我们绑定一个处理函数,当收到信号的时候不立即结束程序
而是先执行我们绑定的处理函数.

多线程多协程的设计

一般多线程或者多协程程序在设计的时候应该设计一个程序终止标记,当收到终止信号的时候
让终止标记状态由False改为True. 运行的程序设计为 只有终止标记在False状态下才能够运行

如果有需要休眠的协程,那么还应该给协程增加一个休眠标记,当程序休眠的时候休眠标记设为True,收到终止信号的时候,不仅仅把终止标记设为True,还要把休眠中的协程结束掉.

休眠的实现:"使用一个死循环包括代码 比如 "

在创建协程的时候,设置一个字典,对应协程的休眠状态

g = gevent.spawn(self._g_fetcher, feed_name)   # _g_fetcher 去取数据
g.link_exception(self._link_exception_callback)  # 如果出现了解决不了的异常而导致协程死亡的话,会触发该回调函数
task_list.append(g)
self.task_dict[feed_name] = [g, False]

休眠的时候把休眠标记设为True

 def _g_fetch_feeds_cfg(self):
        logger.info("start")

        while not self.terminated_flag:
            for feed_name in MTW_RSS_CFG:
                serch_id = MTW_RSS_CFG[feed_name]["serch_id"]
                # 去获取关键字,如果没有登陆成功,没有获取到关键字,返回的是空列表
                keywords_list = self._fecth_feeds_cfg(serch_id)
                logger.info("_g_fetch_feeds_cfg function get keywords_list:{}".format(keywords_list))
                logger.info("{0} got keywords_list {1}".format(feed_name, len(keywords_list)))
                if not keywords_list:
                    continue
                self.ac_keyword_cfg[feed_name] = BuildAC(None, keywords_list, logger)
            logger.info("feed_name对应的ac_keyword_cfg 数据:{}".format(self.ac_keyword_cfg))
            if self.terminated_flag:
                break

            self.keyword_cfg_fetched = True
            self.task_dict["fetch_feeds_cfg"][1] = True
            gevent.sleep(86400)
            self.task_dict["fetch_feeds_cfg"][1] = False

        logger.info("stop")

收到终止标记的时候设置终止标记为true,并且轮询杀死所有正在休眠的协程

    def _term_handler(self, signal_num, frame):
        logger.info("Get TERM signal {0}".format(signal_num))
        self.terminated_flag = True
        self._kill_sleep_gevent()   # 轮询结束休眠的协程

    def _kill_sleep_gevent(self):
        # 不采用 gevent.signal(signal.SIGQUIT, gevent.kill)
        # 仅想让在sleep的协程强制退出,其他正常退出  PS: 如果sleep函数能被唤醒最好
        for name, task in self.task_dict.items():
            # task_dict结构 {"feedname":[g,flase],...}
            # name是第几个推送任务,对应的变量名是feedname
            # task是执行该任务的协程,以及该协程是否是睡眠状态 组成的列表
            is_sleep = task[1]
            if is_sleep:
                _g = task[0]
                gevent.kill(_g)
                task[1] = False   # 杀死协程后,修改字典的状态,防止下次遍历的时候,再次杀死不存在的协程引起错误
                logger.warning("kill gevent {0} bcz it's sleeping".format(name))

小示例

import json
import signal
import time
from config import P2P_MYSQL_CONFIG,config_redis,logger_root,queue_key
from mysql_conn import MysqlConn
from redis_client import RedisClient
import os


class Proccess(object):
    def __init__(self):
        self.mysql_client = MysqlConn(logger_root,P2P_MYSQL_CONFIG)
        self.redis_client = RedisClient(config_redis)
        self.start_id = 1
        if os.path.exists("./data/last_start_id.txt"):
            with open("./data/last_start_id.txt","r",encoding="utf-8",newline="\n") as f:
                self.start_id = int(f.readline().strip())
        self.term_flag = False

    def start(self):
        signal.signal(signal.SIGTERM, self._term_handler)
        signal.signal(signal.SIGINT, self._term_handler)
        i = 0
        while not self.term_flag:
            sql_str = "select * from yq_p2p_info where id > %d order by id asc limit 10"
            sql_params = (self.start_id,)
            res = self.mysql_client.execute_query(sql_str,sql_params)
            if res is None:
                logger_root.error("mysql select error")
                continue
            if not res:
                time.sleep(3)
                logger_root.warning("no data from mysql ,sleep 3s ...")
                continue
            for data in res:
                if not self.term_flag:
                    try:
                        self.start_id = data["id"]
                        dict1 = {
                            "url" : data["url"],
                            "pub_time" : str(data["pub_time"]),
                            "source": data["source"],
                            "title": data["title"],
                            "content": data["abstract"],
                            "full_content": data["content"],
                        }
                    except Exception as e:
                        logger_root.error("get data error :%s"%e)
                        continue
                    try:
                        self.redis_client.rpush(queue_key,json.dumps(dict1,ensure_ascii=False))
                        logger_root.info("rpush success")
                    except Exception as e:
                        logger_root.error("rpush key error:{}".format(e))
                        continue


            i += 1
            if i % 100 == 0:
                self._save_file()
        self._save_file()

    def _term_handler(self,signal_num,frame):
        logger_root.info("get Termination num is {}".format(signal_num))
        self.term_flag = True


    def _save_file(self):
        with open("./data/last_start_id.txt", "w", encoding="utf-8", newline="\n") as f:
            f.write(str(self.start_id))

if __name__ == '__main__':
    task = Proccess()
    task.start()

推荐阅读更多精彩内容