pythonmultithreadingpyqtmultiprocessingterminate

Terminate a long-running Python command within a PyQt application


I have a PyQt GUI which I use to start long-running computations in Python. Here's a minimal example:

import sys
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QDialog,
                             QVBoxLayout, QPushButton, QDialogButtonBox)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        button = QPushButton("Start", self)
        button.clicked.connect(self.long_task)
        self.setGeometry(300, 300, 300, 200)
        self.show()

    def long_task(self):
        dialog = QDialog(self)
        vbox = QVBoxLayout(dialog)
        label = QLabel("Running...")
        button = QDialogButtonBox(QDialogButtonBox.Cancel)
        vbox.addWidget(label)
        vbox.addWidget(button)
        dialog.open()
        time.sleep(10)  # long task, external function
        dialog.close()

app = QApplication(sys.argv)
main = MainWindow()
app.exec_()

From the main window, I can start a task by clicking the button. Then a modal dialog pops up and the task starts. It is okay if the GUI is blocked (I know I can prevent freezing the GUI thread by putting the task in a separate worker thread, but this is not the point). Critically, I want to be able to hit the "Cancel" button to terminate the task. Alternatively, since the long-running tasks are always Python commands, I could also live with terminating the task with Ctrl+C.

I cannot change the long-running Python command: i.e. I can't break it up into tiny pieces and use a state variable in combination with threading, as is sometimes suggested. The alternative (pressing Ctrl+C) also doesn't work since PyQt doesn't seem to register it (even though the Python interpreter should while it is running the task).


Solution

  • The simplest way to do this is to use multiprocessing. This will allow you to run a task (or group of tasks) concurrently and terminate processing at any time. However, make sure you read the programming guidelines to understand how to use the module effectively. In particular, although the terminate method works fine for self-contained tasks, it should not be used with multiple tasks that use shared resources.

    Here is a simple demo based on your example:

    import sys
    import time
    from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QDialog,
                                 QVBoxLayout, QPushButton, QDialogButtonBox)
    
    from multiprocessing import Pool
    
    def long_task():
        for x in range(10):
            print('long task:', x)
            time.sleep(1)
        return 'finished'
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            button = QPushButton("Start", self)
            button.clicked.connect(self.long_task)
            self.setGeometry(300, 300, 300, 200)
            self.show()
    
        def long_task(self):
            dialog = QDialog(self)
            vbox = QVBoxLayout(dialog)
            label = QLabel("Running...")
            button = QDialogButtonBox(QDialogButtonBox.Cancel)
            button.rejected.connect(dialog.close)
            vbox.addWidget(label)
            vbox.addWidget(button)
            pool = Pool()
            def callback(msg):
                print(msg)
                dialog.accept()
            pool.apply_async(long_task, callback=callback)
            if dialog.exec_() == QDialog.Rejected:
                pool.terminate()
                print('terminated')
        
    app = QApplication(sys.argv)
    main = MainWindow()
    app.exec_()