文章目录
前言
面试的时候经常会被问到process信号处理的一些细节,还有process启动的一些细节,如何吊打面试官,让他不敢还嘴呢? 我们可以通过langchain-chatchat 的代码来详细了解信号处理。
代码是startup.py 的 async def start_main_server(): 方法,大家可以先去看一下。我们下面的例子是对它的一个改编
一、示例代码
import os
import time
from multiprocessing import Process
from time import sleep
import signal
def handler(signalname):
def f(signal_received, frame):
raise KeyboardInterrupt(f"{signalname} received")
return f
def signal_handler(sig, frame):
print(f"Received signal {sig} in process {os.getpid()}")
# 在这里执行自定义的清理操作
# 例如关闭文件、释放资源等
# 这个示例中,我们简单地退出程序
# sleep(10)
exit(0)
def task():
# 打开文件
file_handle = open("example.txt", "w")
def task_signal_handler(sig, frame):
print("Ctrl+C pressed. Exiting gracefully...")
# 在这里关闭打开的文件
if file_handle:
print("Closing file...")
file_handle.close()
exit(0)
signal.signal(signal.SIGINT, task_signal_handler)
# 打印当前进程id
print("pid:", os.getpid())
print("task")
sleep(10)
# 打印当前时分秒
time_str = time.strftime("%H:%M:%S", time.localtime())
print(time_str)
if __name__ == "__main__":
# 注册信号处理函数
# signal.signal(signal.SIGINT, signal_handler)
print("Press Ctrl+C to exit gracefully.")
# 打印当前进程id
print("pid:", os.getpid())
signal.signal(signal.SIGINT, handler("SIGINT"))
signal.signal(signal.SIGTERM, handler("SIGTERM"))
p1 = Process(target=task, daemon=True)
try:
p1.start()
print("执行下一个任务")
p1.join()
print("执行完毕")
except KeyboardInterrupt as e:
print(e)
finally:
print("Cleaning up...")
# p1.kill()
p1.terminate()
p1.join()
print(f"Process status: {p1.is_alive()}")
1. 注册信号
大家可以看到这里有2种写法
signal.signal(signal.SIGINT, task_signal_handler)
signal.signal(signal.SIGINT, handler("SIGINT"))
实际上,注册信号的时候,是不能给后面的方法增加自定义参数的。那么为什么第二种可以增加一个自定义参数并传值SIGINT呢?
2.信号处理函数
1)普通写法
def signal_handler(sig, frame):
print(f"Received signal {sig} in process {os.getpid()}")
# 在这里执行自定义的清理操作
# 例如关闭文件、释放资源等
# 这个示例中,我们简单地退出程序
# sleep(10)
exit(0)
2)闭包写法
def handler(signalname):
def f(signal_received, frame):
raise KeyboardInterrupt(f"{signalname} received")
return f
闭包方法实际上是把一个f(signal_received, frame)作为函数传递出去,也就是在
signal.signal(signal.SIGINT, handler("SIGINT"))
中最终第二个参数等同于信号注册函数需要的函数格式。这样就巧妙的利用闭包增加了一个自定义参数
3. 对终止信号的重写
1)终止前关闭资源
def task_signal_handler(sig, frame):
print("Ctrl+C pressed. Exiting gracefully...")
# 在这里关闭打开的文件
if file_handle:
print("Closing file...")
file_handle.close()
exit(0)
2)不关闭只抛出异常
def handler(signalname):
def f(signal_received, frame):
raise KeyboardInterrupt(f"{signalname} received")
return f
这样可以让别的地方接收异常,然后再处理例如
try:
p1.start()
print("执行下一个任务")
p1.join()
print("执行完毕")
except KeyboardInterrupt as e:
print(e)
finally:
print("Cleaning up...")
# p1.kill()
p1.terminate()
p1.join()
print(f"Process status: {p1.is_alive()}")
二、补充知识点
1. signal.SIGTERM 和 signal.SIGINT的区别
signal.SIGTERM
和 signal.SIGINT
都与终止程序相关,但它们并不完全相同。
signal.SIGINT
是由键盘输入产生的信号,通常通过按下Ctrl+C
来触发,用于请求程序中断。signal.SIGTERM
是由操作系统发送的终止信号,通常用于请求程序终止。在 Unix/Linux 系统中,kill
命令默认发送SIGTERM
信号。
虽然它们都可以用于请求程序终止,但它们的使用场景略有不同:
- 通常情况下,
SIGINT
更多地用于用户主动请求程序终止,比如在终端中按下Ctrl+C
。 - 而
SIGTERM
则更多地由系统或其他程序发送,用于通知程序终止,但程序可以选择是否立即响应这个信号,或是在一段时间后进行优雅地关闭。
2. Process(target=task, daemon=True)里的daemon
Process的daemon参数的默认值是False,当它是false的时候,如果创建的子进程没有执行完,主进程会一直等待。当然要注意的是虽然会阻塞主进程结束,但并不会阻塞在如下代码 p1.start() 这里,在程序运行的时候,不管p1进程有没有执行完,都会立即往下走,也就是会输出“执行到这里”。它的
p1 = Process(target=task, daemon=False)
p1.start()
print('执行到这里')
而当daemon=True 时,不会阻塞主进程结束,主进程可以立即结束。如果不想主进程结束,需要增加p1.join()来等待子进程结束。要注意p1.join是会阻塞下面语句执行的。所以要想启动多个子进程,一定要把多个子进程.start(), 写在上面,而把 join 写在最下面。例如:
p1 = Process(target=task, daemon=False)
p1.start()
p1 = Process(target=task, daemon=False)
p2.start()
p1.join()
p2.join()
总结
通过今天的学习我们能了解到闭包的一种巧妙用法、信号接收与处理,以及各自的好处。这样我们写代码的时候可以考虑的更多,而不只是被动依赖于操作系统对进程的处理,减少类似内存泄露、资源浪费等的问题的发生。