Search code examples
pythonconcurrencypyqtqthread

PyQt A problem of wait() in QThread using moveToThread() method


I'm trying to write a multi-thread program with QThread in PyQt6.
The example code is below.
I create two threads by moveToThread() method and expect to join both of them after finish, but the result is crushing.
I know the other way is create subclass of QThread, it's easier to write, but I still want to understand why moveToThread() cannot do that.
Thank you!

import sys
import logging
from functools import partial
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *

logging.basicConfig(format="%(message)s", level=logging.INFO)

class MyWork(QObject):
  finished = pyqtSignal()
  def run_work(self, obj_name):
    # sleep 3 secs and finish
    for i in range(3):
      QThread.sleep(1)
      logging.info(f'{obj_name}: {i} sec.')
    self.finished.emit()

class MyWindow(QWidget):
  def __init__(self):
    super().__init__()
    self.setWindowTitle('MyWork Example')

app = QApplication(sys.argv)
form = MyWindow()
form.show()

# create two threads
th1 = QThread()
wo1 = MyWork()
wo1.moveToThread(th1)
th1.started.connect(partial(wo1.run_work, 'obj1'))
wo1.finished.connect(th1.quit)
th1.start()

th2 = QThread()
wo2 = MyWork()
wo2.moveToThread(th2)
th2.started.connect(partial(wo2.run_work, 'obj2'))
wo2.finished.connect(th2.quit)
th2.start()

# join two threads and finish
th1.wait()
th2.wait()
logging.info('All threads finished.')

sys.exit(app.exec())

the output:

obj1: 0 sec.
obj2: 0 sec.
obj2: 1 sec.
obj1: 1 sec.
obj2: 2 sec.
obj1: 2 sec.

Solution

  • What you're seeing is caused by the fact that cross-thread signals call their connected functions in the thread of the receiver.

    Remember that a QThread (just like a Thread object in python) is not "the thread", but the interface to access and run it.

    When you do this:

    wo1.finished.connect(th1.quit)
    

    the result is that quit() will be called in the thread in which th1 was created, which is the main thread.

    Since wait() blocks the event loop of the thread in which it was called, the call to quit() is queued and never processed.

    For this specific case, the solution is to use a direct connection for the signal:

    wo1.finished.connect(th1.quit, Qt.DirectConnection)
    # ...
    wo2.finished.connect(th2.quit, Qt.DirectConnection)
    

    By doing this, quit() will be called from the actual thread, allowing its immediate processing.

    Note that yours is a very peculiar case that normally won't be used. You normally connect the worker "finished" signal to both quit() and wait() (in this specific order), or call those functions in the same order when you actually need to quit, or connect the QThread finished signal to the function that eventually will print the completion of the thread execution.