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

PyQt多线程回调未执行求助:线程结束后回调函数未触发

PyQt多线程回调未执行问题排查与修复

问题描述

开发了一个启动5个线程的PyQt程序,每个线程向控制台输出Hello World!,并在线程结束时调用回调函数输出Goodby folks。程序统计每个线程运行时间及总运行时间,总运行时间与最长线程运行时间一致,但5次回调的Goodby folks输出全部缺失。

当前输出

saying Hello World! took 0.126 s
saying Hello World! took 0.439 s
saying Hello World! took 0.645 s
saying Hello World! took 0.994 s
saying Hello World! took 1.469 s

total runtime is 1.479 s

相关代码

import random
import sys
import time
from typing import Callable

from PyQt5.QtCore import QCoreApplication, QThread, QObject, pyqtSignal


class MeasureRuntime:
    def __enter__(self):
        self.__start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.__stop = time.perf_counter()
        self.elapsed = self.__stop - self.__start
        return True


class MyQtThreads(QObject):

    def __enter__(self):
        self.__running_threads: list = []
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.__waitForThreadsListEmpty()
        return True

    def __waitForThreadsListEmpty(self):
        while self.__running_threads:
            for thread in self.__running_threads:
                if thread.isFinished():
                    self.__running_threads.pop(self.__running_threads.index(thread))

    def doInThread(self, fn, *args, callback: Callable = None, **kwargs) -> int:
        thread = MyQtThread(fn, *args, **kwargs)
        if callback:
            thread.SIG_FINISHED.connect(callback)
        self.__running_threads.append(thread)
        thread.start()
        return id(thread)


class MyQtThread(QThread):
    SIG_FINISHED = pyqtSignal(object)

    def __init__(self, fn, *args, **kwargs):
        super().__init__()

        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    def run(self):
        ret = self.fn(*self.args, **self.kwargs)
        result_info = {"thread_id": id(self), "args": ret}
        self.SIG_FINISHED.emit(result_info)


def say_hello():
    with MeasureRuntime() as rt:
        random.seed()
        time.sleep(random.random()*3)
        print(f'saying Hello World! took ', end='')
    print(f'{rt.elapsed:.3f} s')


def say_adieu(args):
    print('Goodby folks')


class Main(QThread):

    def __init__(self):
        super().__init__()

    def run(self):

        print()
        with MeasureRuntime() as rt:

            with MyQtThreads() as threads:
                for _ in range(5):
                    threads.doInThread(say_hello, callback=say_adieu)

        print(f'\ntotal runtime is {rt.elapsed:.3f} s')


if __name__ == '__main__':
    qcoreapp = QCoreApplication(sys.argv)
    thread = Main()
    thread.finished.connect(qcoreapp.exit)
    thread.start()
    qcoreapp.exec()

问题原因

  1. 信号槽线程上下文错误:SIG_FINISHED信号在子线程中发射,而say_adieu作为普通函数连接时,Qt默认尝试在子线程执行回调,但子线程没有Qt事件循环,导致信号无法被处理。
  2. 线程对象提前销毁:MyQtThread对象是doInThread中的局部变量,线程完成后可能被Python垃圾回收销毁,此时信号还未传递到回调就丢失。
  3. 线程等待逻辑缺陷:__waitForThreadsListEmpty方法遍历过程中直接修改列表会导致遍历异常,且未调用thread.wait()确保线程完全结束,仅靠isFinished()判断可能存在线程未完全清理就被移除的情况。

修复方案

1. 修正线程等待逻辑

修改MyQtThreads的__waitForThreadsListEmpty方法,复制列表避免遍历异常,并调用wait()确保线程完全结束:

def __waitForThreadsListEmpty(self):
    while self.__running_threads:
        # 复制列表防止遍历中修改原列表引发的问题
        for thread in self.__running_threads.copy():
            if thread.isFinished():
                thread.wait()  # 等待线程资源完全释放
                self.__running_threads.remove(thread)

2. 指定队列连接信号槽

在连接信号时使用Qt::QueuedConnection,让回调在主线程的事件循环中执行,即使子线程销毁也能保证信号被处理:

from PyQt5.QtCore import Qt

def doInThread(self, fn, *args, callback: Callable = None, **kwargs) -> int:
    thread = MyQtThread(fn, *args, **kwargs)
    if callback:
        # 指定队列连接,回调将在主线程执行
        thread.SIG_FINISHED.connect(callback, Qt.QueuedConnection)
    self.__running_threads.append(thread)
    thread.start()
    return id(thread)

3. 补充函数返回值(可选)

say_hello无返回值,导致result_info["args"]为None,补充返回值让逻辑更完整:

def say_hello():
    with MeasureRuntime() as rt:
        random.seed()
        time.sleep(random.random()*3)
        print(f'saying Hello World! took ', end='')
    elapsed = rt.elapsed
    print(f'{elapsed:.3f} s')
    return elapsed

修复后输出示例

saying Hello World! took 0.126 s
Goodby folks
saying Hello World! took 0.439 s
Goodby folks
saying Hello World! took 0.645 s
Goodby folks
saying Hello World! took 0.994 s
Goodby folks
saying Hello World! took 1.469 s
Goodby folks

total runtime is 1.479 s

内容的提问来源于stack exchange,提问作者Holger

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 22:09:52