[Python学习日记-86] 并发编程之多进程 —— 守护进程
简介
在 Python 的并发编程领域,多进程模块 multiprocessing 是处理 CPU 密集型任务的重要工具。其中,守护进程(Daemon Process)作为一种特殊进程类型,具有自动清理、资源隔离等特性。本文将从底层原理到实战案例,深入剖析守护进程的使用场景、实现方式及注意事项。
守护进程的核心概念
守护进程是一种在后台运行的服务型进程,不与终端交互,通常用于执行周期性任务或提供基础服务。举个例子,主进程创建子进程,然后子进程设置成守护进程,那守护进程就好比明朝末代皇帝崇祯皇帝身边的老太监,崇祯皇帝上吊死后老太监也就跟着殉葬了。
关于守护进程的核心特性有以下几点:
- 生命周期绑定:父进程终止时,守护进程会被强制终止
- 资源隔离:独立于父进程的内存空间
- 无终端关联:无法接收用户输入
- 无法开启子进程:主要是为了避免守护进程的子进程变成孤儿进程,从而造成资源浪费,如果开启将会抛出异常(AssertionError: daemonic processes are not allowed to have children)
注意:如果是 Unix/Linux 系统的话,传统的守护进程需要通过两次 fork() 来实现,而 Python 的 multiprocessing 模块已封装底层细节了
守护进程的实现方式
一、基本使用方法
守护进程有两种实现方式,分别如下:
方式一:在定义进程时就进行定义
# 设置daemon属性
p = Process(target=target_func, daemon=True)
p.start()
方式二:在定义完进城后开启进程前
p = Process(target=target_func)
p.daemon = True # 设置 p 为守护进程,禁止 p 创建子进程,并且父进程代码执行结束,p 随即终止运行
p.start()
注意:无论哪一种方式实现的都需要在进程开启前(p.start())前设置
下面演示一个简单的调用代码,代码如下
from multiprocessing import Process
import time
def task(name):
print('%s is runing' % name)
time.sleep(2)
print('%s is end' % name)
if __name__ == '__main__':
p = Process(target=task,args=('子进程1',))
p.daemon = True # 开启守护进程
p.start()
print('主') # 主进程结束守护进程也会立刻结束
代码输出如下:
并没有输出守护进程中的内容,这是因为 p.start() 只是一个代理人,真正的操刀手(操作系统)还没有来得及执行守护进程的第一条输出就因为主进程的结束而导致守护进程的结束了。
二、高级配置技巧
代码如下:
from multiprocessing import Process
import time
def task(name):
print('%s is runing' % name)
time.sleep(2)
print('%s is end' % name)
# 多守护进程管理
def manager_daemon():
processes = [Process(target=task, args=('子进程%s' % _,), daemon=True) for _ in range(3)]
for p in processes:
p.start()
# 让主进程等待子进程执行完毕
for p in processes:
p.join()
if __name__ == '__main__':
manager_daemon()# 开启守护进程
print('主')
代码输出如下:
三、练习题
思考下列代码的执行结果有可能有哪些情况?为什么?
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == '__main__':
p1 = Process(target=foo)
p2 = Process(target=bar)
p1.daemon = True
p1.start()
p2.start()
print("main-------")
代码输出如下:
我们来分析一下,从输出看守护进程 p1 是完全没有执行的,这和前面使用方法时遇到的现象是一样的,是因为系统还没来得及执行主进程就已经结束了,那么守护进程 p1 就跟着一起殉葬了,所以什么都来不及执行,而 p2 作为非守护进程就不管主进程了,只要我没执行完就会继续执行,管你主进程执行完没。如果你的电脑执行速度快有很可能会出现 123 打印在 main------- 的前面的情况,但是守护进程根本不可能在 main------- 后进行输出。
典型应用场景
那守护进程到底有什么用呢?如果我们有两个任务需要并发执行,那么开一个主进程和一个子进程分别去执行就没什么问题了;如果子进程的任务在主进程任务结束后就没有存在的必要了,那么该子进程应该在开启前就被设置成守护进程。主进程代码运行结束时,守护进程也随即终止。
守护进程可以应用到以下场景:
- 日志服务:实时写入日志而不阻塞主线程,而且该服务当主进程不存在了也没必要存在了,因为不会再有日志产生了
- 监控系统:周期性检查资源状态,同样的当主进程不存在了也没必要存在了,因为监控对象不存在了
- 缓存清理:自动回收不再使用的缓存,当主进程结束后会随着主程序清理缓存,这样可以更好的减少资源浪费
- 异步 I/O 辅助:配合协程处理阻塞操作
关键注意事项
前面在介绍用法时也提到了一些注意事项,下面总的一起来介绍一下在使用守护进程是应该注意的一些关键事项。
一、生命周期控制
启动顺序:必须在 p.start() 前设置 daemon=True 或 p.daemon=True
父进程存活:守护进程必须在父进程启动后创建
强制终止:父进程退出时不会执行 finally 块
在 Python 中,finally 代码块是用于定义无论程序是否发生异常都必须执行的代码。它通常用于清理资源、关闭打开的文件、释放锁等操作。在 try-except-finally 语句中,无论是否发生异常, finally 代码块始终会执行。然而,当我们在 Pycharm 中点击“stop”按钮时,程序会立即停止执行,不会等待当前代码块执行完毕,也不会触发任何异常处理机制。因此,finally 代码块也不会被执行。
二、资源管理陷阱
文件操作:可能导致数据丢失(需使用同步机制)
网络连接:突然终止可能造成端口占用
子进程创建:守护进程不能创建新的子进程
# 错误示例:守护进程中创建子进程
from multiprocessing import Process
import time
def bad_daemon():
p = Process(target=lambda: print("子子进程")) # 会抛出AssertionError
p.start()
if __name__ == '__main__':
p = Process(target=bad_daemon, daemon=True)
p.start()
p.join()
三、跨平台差异
特性 | UNIX/Linux/macOS | Windows |
---|---|---|
进程创建方式 | fork | spawn |
信号处理 | 支持 SIGTERM | 部分信号不支持 |
最大进程数 | 受系统限制 | 受内存限制 |
四、调试与监控
日志记录:守护进程的标准输出不会显示在终端
性能监控:使用 psutil 库监控进程状态
异常处理:需配合 try...except... 来捕获异常
# 带异常处理的守护进程
from dbm import error
from multiprocessing import Process
import time
def safe_worker():
try:
n = 0
error11 # error 发生的地方
while n < 10:
n += 1
except Exception as e:
print(f"守护进程异常: {e}")
if __name__ == '__main__':
p = Process(target=safe_worker, daemon=True)
p.start()
p.join()