PYQT5|Pyside2 background thread and signal mode multithreading to prevent interface jamming

Posted by kristofferlc on Wed, 08 Dec 2021 23:19:16 +0100

PYQT5|Pyside2 background thread and signal mode multithreading to prevent interface jamming

Interface blocking problem

In the previous exercise, we developed an HTTP interface testing tool similar to Postman.

The specific code for sending the request message is as follows

    def sendRequest(self):

        method = self.ui.boxMethod.currentText()
        url    = self.ui.editUrl.text()
        payload = self.ui.editBody.toPlainText()

        # Get message header
        headers = {}
        # Some processing of message headers is omitted here

        req = requests.Request(method,
                               url,
                               headers=headers,
                               data=payload
                               )

        prepared = req.prepare()

        self.pretty_print_request(prepared)
        s = requests.Session()
        
        try:
            # Send request and receive response message
            r = s.send(prepared)
            # Print out the response message
            self.pretty_print_response(r)
        except:
            self.ui.outputWindow.append(
                traceback.format_exc())

Here's a question:

We   Click the send button   If the server receives and processes HTTP messages slowly, it will take a long time for the send method in the following line of code to return.

r = s.send(prepared)

What problems does this lead to?

Assuming that the response message from the server is received after 10 seconds, the interface will   dead   10 seconds.

During this period, you click the interface without any reaction.

Why?

reason

This is because our current code is executed in the main thread.

The last code

app.exec_()

In fact, it will let the main thread enter an endless loop, which continuously handles the events of user operation.

When we click the send button, Qt's core code will accept the click event and call the corresponding slot function to process it.

Because our code makes such a setting

        # signal processing 
        self.ui.buttonSend.clicked.connect(self.sendRequest)

Specifies that clicking the send button is handled by the sendRequest method.

If the sendRequest can receive the corresponding from the server soon, the sendRequest can be returned quickly.

After returning, the whole program enters app.exec again_ () receives various events and calls corresponding functions to process them. The interface will not freeze, because all the events of the operation interface can be handled in time.

However, if the sendRequest takes a long time to return, the whole program will stop at the following line of code during this time

r = s.send(prepared)

Naturally, there is no chance to deal with other user interface events. Of course, the program is dead.

Sub thread processing

A typical solution is to use multithreading.

About Python's multithreading, You can click here to refer to our tutorial

The modification code is as follows

    def sendRequest(self):

        method = self.ui.boxMethod.currentText()
        url    = self.ui.editUrl.text()
        payload = self.ui.editBody.toPlainText()

        # Get message header
        headers = {}
        # Some processing of message headers is omitted here

        req = requests.Request(method,
                               url,
                               headers=headers,
                               data=payload
                               )

        prepared = req.prepare()

        self.pretty_print_request(prepared)
        s = requests.Session()

        # Create a new thread to execute the send method,
        # The server is slow and will only block in new threads
        # Does not affect the main thread
        thread = Thread(target = self.threadSend,
                        args= (s, prepared)
                        )
        thread.start()

    # New thread entry function
    def threadSend(self,s,prepared):

        try:
            r = s.send(prepared)
            self.pretty_print_response(r)
        except:
            self.ui.outputWindow.append(
                traceback.format_exc())

In this way, by creating a new thread to execute the send method, no matter how slow the server response is, it will only block in the new thread

After the main thread starts a new thread, it will continue to execute the following code and return to the event loop processing of Qt. It can respond to the user's operation and will not freeze.

VIP practical class students please contact the teacher to obtain a complete code example.

The sub thread sends a signal to update the interface

Click here to learn the following while watching the video explanation

In the above example, we operate the interface in the sub thread, as shown in the following code

    def threadSend(self,s,prepared):

        try:
            r = s.send(prepared)
            # Output content to the interface in a new thread
            self.pretty_print_response(r)
        except:
            # Output content to the interface in a new thread
            self.ui.outputWindow.append(
                traceback.format_exc())

Qt recommendations:   Operate the interface only in the main thread  .

The direct operation interface of another thread may lead to unexpected problems, such as incomplete output display and even program crash.

However, we do often need to update the interface in the child thread. For example, the sub thread is a crawler, and the crawled data is displayed on the interface.

What shall I do?

At this time, the recommended method is to use signals.

We have seen before that various Qt controls can send signals, such as being clicked, input, etc.

We can also customize the class. As long as this class inherits the QObject class, it can send various Qt signals defined by itself. The specific methods are as follows:

  • Customize a Qt QObject class, which encapsulates some custom Signal signals

    How to encapsulate a custom Signal? Refer to the following example code.

    A Signal is defined as a static attribute of this class, and the value is Signal instance object.

    Can define   Multiple   Signal static property, which corresponds to that this type of object can emit   varied   Signal.

    Note: the type specified by the initialization parameter of the Signal instance object is the parameter data type passed when the Signal object is sent. Because the underlying Qt is developed in C + +, the type must be specified.

  • Define the function executed by the main thread to process the Signal signal (through the connect method)

  • When a new thread needs an operation interface, it sends a signal through a custom object

    Send a Signal through the emit method of the Signal object, and the parameters of the emit method pass the necessary data. The parameter type follows the type specified when defining Signal.

  • The Signal processing function of the main thread is triggered to execute, obtain the parameters in Signal, and perform necessary interface update operations

An example code is as follows

from PySide2.QtWidgets import QApplication, QTextBrowser
from PySide2.QtUiTools import QUiLoader
from threading import Thread

from PySide2.QtCore import Signal,QObject

# Custom signal source object types must inherit from QObject
class MySignals(QObject):

    # Define a signal. The two parameter types are QTextBrowser and string
    # When calling the emit method to send a signal, the incoming parameter must be the parameter type specified here
    text_print = Signal(QTextBrowser,str)

    # Other kinds of signals can also be defined
    update_table = Signal(str)

# instantiation 
global_ms = MySignals()    

class Stats:

    def __init__(self):
        self.ui = QUiLoader().load('main.ui')

        # Custom signal processing function
        global_ms.text_print.connect(self.printToGui)


    def printToGui(self,fb,text):
        fb.append(str(text))
        fb.ensureCursorVisible()

    def task1(self):
        def threadFunc():
            # Trigger the execution of the processing function in the main thread through Signal's emit
            # The quantity and type of the emit parameter and the defined Signal must be consistent
            global_ms.text_print.emit(self.ui.infoBox1, 'Output content')
        
        thread = Thread(target = threadFunc )
        thread.start()

    def task2(self):
        def threadFunc():
            global_ms.text_print.emit(self.ui.infoBox2, 'Output content')

        thread = Thread(target=threadFunc)
        thread.start()

Topics: Qt PyQt5 PySide2