Holding for the Applause (Signal)

Something that I’ve come across whilst delving into the deepest, darkest areas of QNetworking (and as a result, QThreading) are user cases where the main python thread needs to wait for an async object to complete a task before processing.


One of Qt’s biggest advantages, in my opinion, is how everything is built around the concept async, allowing for such easy creation of QNetworking and dealing with QThreadpools etc.

Thanks to these building blocks, it’s easy to create really intricate connected applications that are fast and robust. Due to the multithreading nature of these objects, there are a few user cases where the main thread needs to hold and wait for something in another thread to finish.

As these objects are not exposing the thread that they run on for efficient reasons, we can’t just call join() on them, but there are plenty of tricks!


In the real world, it’s perfectly reasonable to use QTcpSocket for a client networking interface as it handles so much of the heavy lifting, but complications arise if the code is going connect to a server, send some information, and then closeout.

As the connection happens in another thread, we have no ability to hold the main thread until that point. 

(Keen-eyed developers might know that QTcpSocket has a method for this reason called waitForConnected, but it’s been documented as unstable on Windows)

A simple way to show this is with this snippet with a FakeNetwork class:

import sys

from PySide import QtCore


class FakeNetwork(QtCore.QObject):
    connected = QtCore.Signal()
    
    def __init__(self):
        """
        Constructor
        """
        super(FakeNetwork, self).__init__()
        self.isConnected = False
        self._mockConnectionDelay = QtCore.QTimer()

    def connectToHost(self, hostName):
        """
        Connect to a given host
        
        args:
            hostName (str): The host to connect to
        """
        self._mockConnectionDelay.start(3000) # 3 Second
        self._mockConnectionDelay.timeout.connect(self._handleConnection)
        
    def _handleConnection(self):
        """
        Handles a successful connection to a host 
        """
        self.isConnected = True
        self.connected.emit()


app = QtCore.QCoreApplication(sys.argv)
myFakeConnection = FakeNetwork()
myFakeConnection.connectToHost("fake-hostName")

print myFakeConnection.isConnected


sys.exit(app.exec_())

Running it, the .isConnected prints out False, and the application finishes.
Not quite what we want.

By using a QEvenLoop, we can keep checking the and processing events but holding on the main thread in place until the given signal is emitted or the timeout is hit, whichever is first. Implementing it as a method makes it easily re-useable anywhere in the code.

def blockUntilSignal(trigger, timeout=sys.maxint):
    """
    Block the main thread until the given signal is emited or the timeout hits
    """
    eventLoop = QtCore.QEventLoop()
    timer = QtCore.QTimer()
    timer.start(timeout)
    trigger.connect(eventLoop.quit)
    timer.timeout.connect(eventLoop.quit)
    eventLoop.exec_()

We can combine the two, as well as a single one liner to call it, giving us the final code:

import sys

from PySide import QtCore


class FakeNetwork(QtCore.QObject):
    connected = QtCore.Signal()
    
    def __init__(self):
        """
        Constructor
        """
        super(FakeNetwork, self).__init__()
        self.isConnected = False
        self._mockConnectionDelay = QtCore.QTimer()

    def connectToHost(self, hostName):
        """
        Connect to a given host
        
        args:
            hostName (str): The host to connect to
        """
        self._mockConnectionDelay.start(3000) # 3 Second
        self._mockConnectionDelay.timeout.connect(self._handleConnection)
        
    def _handleConnection(self):
        """
        Handles a successful connection to a host 
        """
        self.isConnected = True
        self.connected.emit()


def blockUntilSignal(trigger, timeout=sys.maxint):
    """
    Block the main thread until the given signal is emited or the timeout hits
    """
    eventLoop = QtCore.QEventLoop()
    timer = QtCore.QTimer()
    timer.start(timeout)
    trigger.connect(eventLoop.quit)
    timer.timeout.connect(eventLoop.quit)
    eventLoop.exec_()


app = QtCore.QCoreApplication(sys.argv)
myFakeConnection = FakeNetwork()
myFakeConnection.connectToHost("fake-hostName")

# Block until Signal
blockUntilSignal(myFakeConnection.connected)
print myFakeConnection.isConnected


sys.exit(app.exec_())

With the final code in place, the code successful blocks and holds until the connection is established allowing for the .isConnected to print True.


We’ve shown a simple and yet powerful way to block the main code whilst we’re waiting for multithreading objects to run their code.

This design could easily be expanded to take in a list of signals and wait until one of them, such as a ‘connected’ signal or an ‘errored’ signal instead.


I hope that given you less reasons to use unleash’s Qt power on your next application!

– Geoff Samuel

Leave a Reply

Your email address will not be published.