Radek D
Radek D

Reputation: 103

PyQt5 QThreads persists in call stack shown by VSCode after thread quitting

What I wonder about is why in debugging mode the Dummy-N thread in VSCode Call Stack Viewer is considered as Running even after closing the thread. I use latest versions of VSCode (1.97.2) and Microsoft Python Debugger (2025.1.2025022401). Python 3.11.6, PyQt5 version 5.15.10.

__main__.py

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from signal_generator.signals import SignalManager

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        self.signalmanager = SignalManager(parent=self)
        
        self.setWindowTitle("Python oscilloscope")

        self.setWindowTitle("Checkable Button Example")
        self.setGeometry(100, 100, 300, 200)

        # Create a button with checkable state
        self.button = QPushButton("Checkable Button", self)
        self.button.setCheckable(True)
        self.button.setGeometry(100, 80, 100, 40)

        # Connect button signal to a slot function
        self.button.clicked.connect(self.on_button_click)
        
        self.channel1_thread = None
        self.channel1_generator = None

    def on_button_click(self):
        if self.button.isChecked():
            self.button.setText("Run background task")
            self.signalmanager.start_signal_generator(1)
        else:
            self.button.setText("Stop background task")
            self.signalmanager.stop_signal_generator(1)
    
    def closeEvent(self, event):
        self.close()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    
    # TESTING

    window.show()
    app.exec_()

signal_generator\signals.py

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

import sys

import debugpy
has_trace = hasattr(sys, 'gettrace') and sys.gettrace() is not None
has_breakpoint = sys.breakpointhook.__module__ != "sys"
isdebug = has_trace or has_breakpoint

class SignalManager:
    def __init__(self, parent):
        self.parent = parent
    
    def start_signal_generator(self, channel: int):
        if channel == 1:
            # Ensure the previous thread is properly finished
            if hasattr(self, "channel1_thread") and self.channel1_thread is not None:
                self.stop_signal_generator(channel)
            # Define worker thread
            self.channel1_thread = QThread()
            self.channel1_generator = SignalGenerator(self.parent)
            self.channel1_generator.moveToThread(self.channel1_thread)
            self.channel1_thread.started.connect(self.channel1_generator.run)
            self.channel1_generator.finished.connect(self.channel1_thread.quit)
            self.channel1_generator.finished.connect(self.channel1_generator.deleteLater)
            self.channel1_thread.finished.connect(self.channel1_thread.deleteLater)
            self.channel1_generator.progress.connect(self._reportProgress)
            self.channel1_thread.start()
            
    def stop_signal_generator(self, channel):
        if channel == 1 and self.channel1_thread is not None and self.channel1_generator.is_running():
            self.channel1_generator.stop()
            self.channel1_thread.quit()
            self.channel1_thread.wait()
            self.channel1_thread = None
    
    def _reportProgress(self):
        """This function will be responsible for plotting updated signal."""
        pass

class SignalGenerator(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal()
    
    def __init__(self, parent, *args, **kwargs):
        super().__init__()
        self.parent = parent
        self.running = True
        
        if "noise_std_dev" in kwargs:
            self.noise_std_dev = kwargs["noise_std_dev"]
    
    def run(self):
        """Long-running task of generating/updating the signal"""
        if isdebug:
            debugpy.debug_this_thread()
        
        while self.running:
            if not self.running:
                break
            
            time.sleep(1)
            
            self.progress.emit()
        
        self.finished.emit()
        self.stop()
    
    def stop(self):
        self.running = False
    
    def is_running(self):
        return self.running

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File in Current Workspace with Qt",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/__main__.py",
            "console": "integratedTerminal",
            "cwd": "${workspaceFolder}",
            "justMyCode": false
        }
}

This minimal working example reproduces the situation I ask about. If one checks the call stack viewer in VSCode Dummy-N thread persists and new one is created when the background task is restarted.

Upvotes: 0

Views: 24

Answers (0)

Related Questions