18

I have the following code that performs a background operation (scan_value) while updating a progress bar in the ui (progress). scan_value iterates over some value in obj, emitting a signal (value_changed) each time that the value is changed. For reasons which are not relevant here, I have to wrap this in an object (Scanner) in another thread. The Scanner is called when the a button scan is clicked. And here comes my question ... the following code works fine (i.e. the progress bar gets updated on time).

# I am copying only the relevant code here.

def update_progress_bar(new, old):
    fraction = (new - start) / (stop - start)
    progress.setValue(fraction * 100)

obj.value_changed.connect(update_progress_bar)

class Scanner(QObject):

    def scan(self):
        scan_value(start, stop, step)
        progress.setValue(100)

thread = QThread()
scanner = Scanner()
scanner.moveToThread(thread)
thread.start()

scan.clicked.connect(scanner.scan)

But if I change the last part to this:

thread = QThread()
scanner = Scanner()
scan.clicked.connect(scanner.scan) # This was at the end!
scanner.moveToThread(thread)
thread.start()

The progress bar gets updated only at the end (my guess is that everything is running on the same thread). Should it be irrelevant if I connect the signal to a slot before of after moving the object receiving object to the Thread.

Taran
  • 12,822
  • 3
  • 43
  • 47
Hernan
  • 5,811
  • 10
  • 51
  • 86
  • 3
    Looks like ekhumoro is right (pyqt/qt doesn't appear to be auto-detecting the connection type correctly unless you explicitly decorate your slots with @pyqtSlot()). However, I wanted to point out that the line `progress.setValue(100)` is thread **unsafe** because you are accessing a Qt GUI object from a thread other than the main thread. The rest of your posted code is thread safe in terms of Qt GUI operations – three_pineapples Dec 29 '13 at 02:34
  • 1
    @three_pineapples. It would be interesting to know if there is a PyQt bug here, or if it's just a peculiarity of how PyQt connects to Python callables. I know that some kind of proxy object is created when `@pyqtSlot` isn't used, but exactly what consequences that has for queued connections, I don't know. – ekhumoro Dec 29 '13 at 03:25
  • 1
    @ekhumoro I think it might be a PyQt4 bug, or at least a deficiency that should be corrected. It certainly doesn't show the same behaviour in PySide (PySide always runs the `scan` function in the QThread regardless of where the signal was conected or how the slot is decorated). I have made a minimilistic example here http://pastebin.com/SqP3WM1z that prints out which thread things are running in. – three_pineapples Dec 29 '13 at 04:10
  • 2
    @three_pineapples. Thanks for the test case. I think I have established why the problem occurs (see my updated answer). Given the way PyQt currently works, I think I would now say that it is a deficiency rather than a bug. Not sure whether it would be possible to correct it, though. – ekhumoro Dec 29 '13 at 20:34

3 Answers3

23

It shouldn't matter whether the connection is made before or after moving the worker object to the other thread. To quote from the Qt docs:

Qt::AutoConnection - If the signal is emitted from a different thread than the receiving object, the signal is queued, behaving as Qt::QueuedConnection. Otherwise, the slot is invoked directly, behaving as Qt::DirectConnection. The type of connection is determined when the signal is emitted. [emphasis added]

So, as long as the type argument of connect is set to QtCore.Qt.AutoConnection (which is the default), Qt should ensure that signals are emitted in the appropriate way.

The problem with the example code is more likely to be with the slot than the signal. The python method that the signal is connected to probably needs to be marked as a Qt slot, using the pyqtSlot decorator:

from QtCore import pyqtSlot

class Scanner(QObject):
    
    @pyqtSlot()
    def scan(self):
        scan_value(start, stop, step)
        progress.setValue(100)

EDIT:

It should be clarified that it's only in fairly recent versions of Qt that the type of connection is determined when the signal is emitted. This behaviour was introduced (along with several other changes in Qt's multithreading support) with version 4.4.

Also, it might be worth expanding further on the PyQt-specific issue. In PyQt, a signal can be connected to a Qt slot, another signal, or any python callable (including lambda functions). For the latter case, a proxy object is created internally that wraps the python callable and provides the slot that is required by the Qt signal/slot mechanism.

It is this proxy object that is the cause of the problem. Once the proxy is created, PyQt will simply do this:

    if (rx_qobj)
        proxy->moveToThread(rx_qobj->thread());

which is fine if the connection is made after the receiving object (i.e. rx_qobj) has been moved to its thread; but if it's made before, the proxy will stay in the main thread.

Using the @pyqtSlot decorator avoids this issue altogether, because it creates a Qt slot more directly and does not use a proxy object at all.

Finally, it should also be noted that this issue does not currently affect PySide.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
0

My problem was solved by movinf the connection to the spot where the worker thread is initialized, in my case because I am accessing an object which only exists after instantiation of my Worker Object class which is in another thread.

Simply connect the signal after the self.createWorkerThread()

Regards

Josep Bigorra
  • 733
  • 2
  • 9
  • 20
-1

This has to do with the connection types of Qt.

http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#connect

http://qt-project.org/doc/qt-4.8/qt.html#ConnectionType-enum

In case both objects live in the same thread, a standard connection type is made, which results in a plain function call. In this case, the time consuming operation takes place in the GUI thread, and the interface blocks.

In case the connection type is a message passing style connection, the signal is emitted using a message which is handled in the other thread. The GUI thread is now free to update the user interface.

When you do not specify the connection type in the connect function, the type is automatically detected.

Windel
  • 499
  • 4
  • 11
  • This does not seem to address the concern at hand, does it? Even, you yourself note that the default is auto (which should work). See the other answer for details. – László Papp Dec 31 '13 at 06:48