You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程Python脚本作为Windows服务运行启动报错排查

问题根因

你的代码存在4个核心逻辑错误,直接触发服务启动失败、残留后台进程的问题:

  • 线程定义位置错误:将两个工作线程定义为Service类的类属性,而非实例属性,类加载阶段就会初始化线程对象,易引发跨实例状态混乱。
  • 停止逻辑完全失效:SvcStop方法中没有调用SvcDoRun阶段创建的正在运行的服务实例,反而重新实例化了一个全新的Service对象执行停止操作,原有运行中的工作线程完全收不到停止信号,成为孤儿进程持续在后台运行。同时原有stop()方法逻辑错误,设置running=False后直接暴力调用exit(),根本没有等待工作线程退出。
  • 运行逻辑死循环:run()方法中while self.running循环没有任何阻塞等待逻辑,第一次成功启动两个线程后,循环会立刻进入下一轮,尝试重复启动已经启动的线程(Python中线程对象只能调用一次start()),直接抛出RuntimeError导致服务主线程崩溃。此时SCM(服务控制管理器)会认为服务进程异常退出,但两个已经启动的非守护线程不会随主线程退出,会继续持有进程资源,导致进程残留。
  • 触发SCM进程校验失败:SCM要求启动的服务进程必须在规定时间内绑定到对应服务控制句柄,你的服务主线程崩溃后,残留进程无法响应SCM的控制请求,SCM尝试重连时就会抛出「连接到非SCM启动的进程」的错误,同时之前启动失败残留的孤儿进程不断累积,就会出现强制杀进程时检测到多个关联进程的现象。
修复方案

按以下步骤修改代码即可解决问题:

  1. 将工作线程改为实例属性,在Service类的构造方法中初始化,用threading.Event实现跨线程的停止信号传递,替代不可靠的running标志位。
  2. 修正SvcStop逻辑,直接操作已运行的服务实例触发停止,不要新建实例。
  3. 重写Service.run()逻辑:启动工作线程后阻塞等待停止信号,不要循环重复启动线程;停止时先触发停止信号,等待两个工作线程退出后再返回。
  4. 强制关闭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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 03:24:17