多线程Python脚本作为Windows服务运行启动报错排查
问题根因
你的代码存在4个核心逻辑错误,直接触发服务启动失败、残留后台进程的问题:
- 线程定义位置错误:将两个工作线程定义为
Service类的类属性,而非实例属性,类加载阶段就会初始化线程对象,易引发跨实例状态混乱。 - 停止逻辑完全失效:
SvcStop方法中没有调用SvcDoRun阶段创建的正在运行的服务实例,反而重新实例化了一个全新的Service对象执行停止操作,原有运行中的工作线程完全收不到停止信号,成为孤儿进程持续在后台运行。同时原有stop()方法逻辑错误,设置running=False后直接暴力调用exit(),根本没有等待工作线程退出。 - 运行逻辑死循环:
run()方法中while self.running循环没有任何阻塞等待逻辑,第一次成功启动两个线程后,循环会立刻进入下一轮,尝试重复启动已经启动的线程(Python中线程对象只能调用一次start()),直接抛出RuntimeError导致服务主线程崩溃。此时SCM(服务控制管理器)会认为服务进程异常退出,但两个已经启动的非守护线程不会随主线程退出,会继续持有进程资源,导致进程残留。 - 触发SCM进程校验失败:SCM要求启动的服务进程必须在规定时间内绑定到对应服务控制句柄,你的服务主线程崩溃后,残留进程无法响应SCM的控制请求,SCM尝试重连时就会抛出「连接到非SCM启动的进程」的错误,同时之前启动失败残留的孤儿进程不断累积,就会出现强制杀进程时检测到多个关联进程的现象。
修复方案
按以下步骤修改代码即可解决问题:
- 将工作线程改为实例属性,在
Service类的构造方法中初始化,用threading.Event实现跨线程的停止信号传递,替代不可靠的running标志位。 - 修正
SvcStop逻辑,直接操作已运行的服务实例触发停止,不要新建实例。 - 重写
Service.run()逻辑:启动工作线程后阻塞等待停止信号,不要循环重复启动线程;停止时先触发停止信号,等待两个工作线程退出后再返回。 - 强制关闭Flask的重加载器和debug模式,避免Flask拉起子进程导致SCM PID校验失败。
修复后完整代码
import threading import logging import sys import win32serviceutil import win32service import servicemanager from flask import Flask # 替换为你自己的Flask app初始化逻辑 app = Flask(__name__) app.config['DEBUG'] = False # 替换为你自己的周期执行任务逻辑,增加停止信号判断 def Mains(stop_event): while not stop_event.is_set(): # 这里写你每隔X秒要执行的逻辑 logging.info("Periodic task running...") # 等待X秒,同时响应停止信号,替换30为你需要的间隔秒数 stop_event.wait(30) class Service(): def __init__(self): # 初始化停止信号 self.stop_event = threading.Event() # 线程改为实例属性,传入stop_event供任务判断退出 self.main = threading.Thread(target=Mains, args=(self.stop_event,), daemon=True) self.server = threading.Thread(target=lambda: app.run(port=9999, use_reloader=False, debug=False), daemon=True) def stop(self): """Stop the service""" # 触发停止信号 self.stop_event.set() # 等待两个工作线程退出,最多等5秒 self.main.join(timeout=5) self.server.join(timeout=5) def run(self): """Main service loop""" logging.info("START MAIN THREAD") self.main.start() logging.info("START SERVER THREAD") self.server.start() servicemanager.LogInfoMsg("Service running...") # 阻塞等待停止信号,不要循环启动线程 self.stop_event.wait() class MyServiceFramework(win32serviceutil.ServiceFramework): _svc_name_ = 'Python Service' _svc_display_name_ = 'Python Service...' _svc_description_ = 'lorem ipsum' def SvcStop(self): """Stop the service""" self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) # 不要新建Service实例,直接调用已有实例的stop方法 self.service_impl.stop() self.ReportServiceStatus(win32service.SERVICE_STOPPED) def SvcDoRun(self): """Start the service; does not return until stopped""" self.ReportServiceStatus(win32service.SERVICE_START_PENDING) self.service_impl = Service() self.ReportServiceStatus(win32service.SERVICE_RUNNING) # Run the service self.service_impl.run() def init(): # 兼容打包为exe后的frozen场景 if hasattr(sys, 'frozen'): servicemanager.Initialize(sys.executable) else: servicemanager.Initialize() if len(sys.argv) == 1: servicemanager.PrepareToHostSingle(MyServiceFramework) servicemanager.StartServiceCtrlDispatcher() else: win32serviceutil.HandleCommandLine(MyServiceFramework) if __name__ == '__main__': logging.basicConfig(level=logging.INFO) init()
额外注意事项
- 如果你用PyInstaller打包成exe,打包命令需要加上
--hidden-import win32timezone,避免win32service相关依赖缺失。 - 重新安装服务前,先执行
TASKKILL /im Service.exe /F把之前残留的孤儿进程全部杀干净,再重新安装启动服务。 - 不要开启Flask的debug模式,debug模式默认会启动两个进程,同样会触发SCM的PID不匹配错误。
内容的提问来源于stack exchange,提问作者pikcahu420
相关产品推荐
相关产品推荐

