0

I have a long running task, which for example's sake I have made an infinite while loop:

def long_task(parent, progress_callback):
    top = 100000
    x = 0
    while True:
        if x < top:
            if not parent.stop:
                progress_callback.emit(x)
                x += 1
            else:
                break
        else:
            x = 0
            progress_callback.emit(x)
            x += 1

I have a Worker class that subclasses QRunnable, and then I can override the run() method with whatever function is passed to the Worker.

class ThreadWorker(QtCore.QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super(ThreadWorker, self).__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = ThreadWorkerSignals()
        self.kwargs['progress_callback'] = self.signals.progress
        self.running = False

    @QtCore.pyqtSlot()
    def run(self):
        self.running = True
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done

I create two instances of Worker within my MainWindow, and pass the same long-running task to each worker. Both workers are added to my MainWindow's QThreadPool and then I call start(worker) on each to begin the worker's run() method. I now have two threads running the infinite loop:

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        ## NOT SHOWING THE REST OF INIT CODE

    def create_workers(self):
        self.worker1 = ThreadWorker(self.long_task, parent=self)
        self.worker1.signals.progress.connect(lambda x: self.long_label_1.setText(str(x)))

        self.worker2 = ThreadWorker(self.long_task, parent=self)
        self.worker2.signals.progress.connect(lambda x: self.long_label_2.setText(str(x)))

        self.threadpool.start(self.worker1)
        self.threadpool.start(self.worker2)
        
        self.stop = False

Please note the self.stop attribute above - this also belongs to the MainWindow class.

All I want to do is break the loop (interrupt the run() method of a worker) when I press a button.

As you can see, I am referencing parent.stop during every iteration of the worker's while loop. Right now, if I press my button, MainWindow's stop attribute turns True and the loop breaks when the worker class sees this change.

    def stop_tasks(self):
        self.stop = True

This works fine and accomplishes my goal, but I am wondering if this is dangerous and if there is a better way to do this? I only ask because it seems risky to reference an outside class attribute from within a separate running thread, and I don't know what could go wrong.

Toakley
  • 182
  • 3
  • 13
  • 1
    It's not "risky" if you are sure that you're doing it properly, but it's also not properly correct, as classes should be self-contained and not try to access elements belonging to other objects that are outside of their "scope". A better approach would be to create that flag in the workers and set those flags in `stop_tasks()`: `self.worker1.stop = True` and so on. – musicamante Jan 26 '22 at 19:57
  • Ok, but keep in mind that the long-running task is called from the worker class, so I would have to reference the worker class from the long-running task in order for your suggestion to work. Is this the proper way to do it? – Toakley Jan 26 '22 at 22:43
  • 1
    The concept is the same, it should be responsibility of the parent to "tell" the child what to do. Again, it's not strictly "forbidden", but the thread (or its "task") should work on its own without interfacing with upper layers of the hierarchy, nor know anything about them. So, in this case, the main thread should "tell" the QRunnable to stop its task, which in turn will actually set the flag for the task. While the result would be theoretically similar, from the OOP point of view (see *encapsulation*) it is a more correct approach as it would make each component independent from its parent. – musicamante Jan 27 '22 at 00:12
  • This does not work for me. When I click the "stop" button, the methods cease for a second, then they continue running as if I had just recalled both of them. Why would this be? – Toakley Jan 27 '22 at 23:56
  • We cannot know why it doesn't work without knowing how you implemented that. – musicamante Jan 28 '22 at 00:09
  • The implementation worked, I was having an unrelated problem. Thanks. – Toakley Jan 31 '22 at 06:27

0 Answers0