© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
J. M. WillmanBeginning PyQthttps://doi.org/10.1007/978-1-4842-7999-1_15

15. Managing Threads

Joshua M Willman1  
(1)
Sunnyvale, CA, USA
 

We have all experienced that moment when running some process such as copying files between directories or launching a new instance of an application causes a program to lag for just a moment and, in some cases, to freeze completely. We are then forced to either wait for the current task to complete or Ctrl+Alt+Delete our way to freedom. When you are creating GUIs, you should be aware of how to handle and have foresight about avoiding these situations.

In this chapter, you will
  • Consider techniques to handle time-consuming processes in PyQt

  • Learn how to implement threading in GUIs with QThread

  • Use the QProgressBar widget for giving visual feedback about a task’s progression

The motivation behind this chapter is twofold: to help you design more robust GUI applications while also informing you how you might be able to handle situations where your applications need to run long processes. Any action that causes event processing to come to a standstill is bad for a user’s experience.

Introduction to Threading

A computer’s performance can be measured by the accuracy, efficiency, and speed at which it can execute program instructions. Modern computers can take advantage of their multicore processors to run those instructions in parallel, thereby increasing the performance of computer applications that have been written to utilize a multicore architecture.

The idea of performing tasks in a synchronous manner where only one task is processed at a time until completion before moving on to the next task can be inefficient, especially for larger operations. What we need is a way to perform operations concurrently. That is where threads and processes come into play.

Threads and processes are not the same thing. Without going too much into the technical jargon, let’s try and understand the differences between the two. A process is an instance of an application that requires memory and computer resources to run. Opening up the word processor on your computer to write an essay is one process. While writing your essay, you need to search on the Internet for information. You now have two separate processes running on your computer independently and in parallel. What happens in one process is not influencing the other. Of course, you have multiple tabs open in the web browser, and each tab is loading and updating information; those tabs are working side by side with the web browser. This is where a thread becomes important.

A thread is essential to the concurrency within an individual process. When a process begins, it only has one thread, and multiple threads can be started within a single process. These threads, just like the processes, are managed by the central processing unit (CPU). Multithreading occurs when the CPU can handle multiple threads of execution concurrently within one process. These threads are independent but also share the process’s resources. Using multithreading allows for applications to be more responsive to user’s inputs while other operations are occurring in the background and to better utilize a system’s resources.

On a system with a CPU with only a single core, true parallelism is actually unachievable. In these instances, the CPU is shared among the processes or threads. To switch between threads, context switches are used to interrupt the current thread, save its state, and then restore the next thread’s state. This gives the user a false appearance of parallelism.

To achieve true parallelism and create a truly concurrent system, a multicore processor would allow threads in a multithreaded application to be assigned to different processors.

Threading in PyQt

Applications based on Qt are event based. When the event loop is started using exec(), a thread is created. This thread is referred to as the main thread of the GUI. Any events that take place in the main thread, including the GUI itself, run synchronously within the main event loop. To take advantage of threading, we need to create a secondary thread to offload processing operations from the main thread.

PyQt makes communicating between the main thread and secondary threads, also referred to as worker threads, simple with signals and slots. This can be useful for relaying feedback, allowing the user to interrupt a process, and for informing the main thread that a process has finished. Since threads utilize the same address space, they can share data very easily.

Be cautious, though. If multiple threads try to access shared data or resources concurrently, this can cause crashes or memory corruption. Deadlock is another issue that can occur if two threads are blocked because they are waiting for resources. PyQt provides a few classes, for example, QMutex, QReadWriteLock, and QSemaphore, for avoiding these kinds of problems.

Note

Python also has a number of modules for handling threading and processing tasks, including _thread, threading, asyncio, and multiprocessing. While you can also use these modules, PyQt’s QThread and other classes allow you to emit signals between the main and worker threads.

Methods for Processing Long Events in PyQt

While this chapter focuses on using QThread, it is also a good idea to keep in mind that there are also other ways that you might want to try before attempting to use threading in your GUI. Implementing threading can lead to problems with concurrency and identifying errors. Combined with signals and slots, PyQt provides a few different ways to handle time-consuming operations.

Choosing which method is best for your application comes down to considering your situation. The following are the main methods, including threading, for handling these kinds of events:
  1. 1.

    If there is a process in your application that is causing it to freeze, check to see if that process can be broken down into smaller steps and perform them sequentially. Manually handle the processing of long operations, and explicitly call QApplication.processEvents() to process pending events. This works best if your operations can be processed using a single thread.

     
  2. 2.

    With QTimer and signals and slots, you can schedule operations to be performed at certain intervals in the future.

     
  3. 3.

    Use QThread to create a worker thread that will perform long operations in a separate thread. Derive a class from QThread, reimplement run(), and use signals and slots to communicate with the main thread. This method can help to avoid blocking the main event loop.

     
  4. 4.

    The QThreadPool and QRunnable classes can be used to divide the work across the CPU cores on your computer. Create a subclass of QRunnable and reimplement the run() function; an instance of QRunnable can then be passed to threads that are managed by QThreadPool. QThreadPool handles the queuing and execution of QRunnable instances for you.

     

There are even other options that may depend upon your application’s requirements. Keep in mind that while using threads could benefit your application, they could also slow it down or cause errors if used incorrectly.

Project 15.1 – File Renaming GUI

Creating and labeling datasets often entails writing Python scripts for labeling thousands of images and data files. Those scripts are generally written to include some kind of visual feedback to the user about how the process is going in the command line.

For the GUI in Figure 15-1, we are going to create a GUI that will allow us to select a local directory and edit the names of files in the folder with the specified extension. The interface includes QTextEdit and QProgressBar widgets as two different means of feedback about the file labeling process. This application also takes advantage of the QThread class so that users are still able to interact with the interface while the operations are being performed in the background.
Figure 15-1

The interface for renaming files in a selected directory

The QProgressBar Widget

The QProgressBar widget visually relays the progress of an extended operation back to the user. This feedback can also be used as reassurance that a process such as a download, installation, or file transfer is still running. Some of the settings that can be controlled include the widget’s orientation and range.

Refer to the project in the following sections for setting up a progress bar.

Explanation for File Renaming GUI

The GUI window contains various buttons and editor widgets that allow the user to manage file renaming. The user can select a directory using a QPushButton and the QFileDialog that appears. The new file name can be entered into a QLineEdit widget. Using a QComboBox, the file extension for the files that need to be changed can also be selected.

The application uses threading to update the progress bar, display information about the files being changed in the text edit, and perform the actual renaming operation. This is all done using signals and slots.

Let’s start by using the basic_window.py script from Chapter 1 as a template. Next, import the Python and PyQt classes in Listing 15-1. The QThread class is part of QtCore.
# file_rename_threading.py
# Import necessary modules
import os, sys, time
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QProgressBar, QLineEdit, QPushButton, QTextEdit,
    QComboBox, QFileDialog, QGridLayout)
from PyQt6.QtCore import pyqtSignal, QThread
style_sheet = """
    QProgressBar{
        background-color: #C0C6CA;
        color: #FFFFFF;
        border: 1px solid grey;
        padding: 3px;
        height: 15px;
        text-align: center;
    }
    QProgressBar::chunk{
        background: #538DB8;
        width: 5px;
        margin: 0.5px
    }
"""
Listing 15-1

Code for imports and the style sheet used in the file renaming GUI

The style sheet is used to modify the appearance of the QProgressBar. Besides changing the look of the progress bar, we can also edit the appearance of the subcontrol chunk in order to create a blocky look to the bars as they update.

For this GUI, let’s create a class that inherits QThread. The Worker class in Listing 15-2 will be used to update the progress bar, update the text edit widget, and actually perform the task of renaming the image files, thereby freeing up the main event loop to perform other tasks. An instance of a QThread class manages only one thread.

Three custom signals are created for updating the progress bar and text edit widgets:
  • update_value_signal – Emits a signal that is used to update the integer value of the progress bar

  • update_text_edit_signal – Used to update the content of the QTextEdit widget. Passes string information about the old file name and the new file name

  • clear_text_edit_signal – Signal that is used to clear the text edit widget if the user stops running the worker thread

# file_rename_threading.py
# Create worker thread for running tasks like updating
# the progress bar, renaming photos, displaying information
# in the text edit widget.
class Worker(QThread):
    update_value_signal = pyqtSignal(int)
    update_text_edit_signal = pyqtSignal(str, str)
    clear_text_edit_signal = pyqtSignal()
    def __init__(self, dir, ext, prefix):
        super().__init__()
        self.dir = dir
        self.ext = ext
        self.prefix = prefix
    def stopRunning(self):
        """Terminate the thread."""
        self.terminate()
        self.wait()
        self.update_value_signal.emit(0)
        self.clear_text_edit_signal.emit()
    def run(self):
        """The thread begins running from here.
        run() is only called after start()."""
        for (i, file) in enumerate(os.listdir(self.dir)):
            _, file_ext = os.path.splitext(file)
            if file_ext == self.ext:
                new_file_name = self.prefix + str(i) +
                    self.ext
                src_path = os.path.join(self.dir, file)
                dst_path = os.path.join(
                    self.dir, new_file_name)
                # os.rename(src, dst): src is original address
                # of file to be renamed and dst is destination
                # location with new name
                os.rename(src_path, dst_path)
                # Uncomment if process is too fast and want to
                # see the updates
                #time.sleep(1.0)
                self.update_value_signal.emit(i + 1)
                self.update_text_edit_signal.emit(
                    file, new_file_name)
            else:
                pass
        # Reset the value of the progress bar
        self.update_value_signal.emit(0)
Listing 15-2

Creating the Worker class that subclasses QThread

The reimplemented QThread method, run(), begins executing the thread. The time-consuming operations – traversing the directory, renaming files, and emitting the signals for updating the QProgressBar and QTextEdit – are performed in run(). However, this method is not called directly. The QThread method start() is used to communicate with the worker thread and begin executing the thread by calling run(). The start() method is called from the MainWindow class method renameFiles() in Listing 15-8.

The stopRunning() slot is used to end the thread’s processes when the user pushes the Stop button in the main window. The terminate() method is used to end the thread, and wait() is used to make sure that the thread ends by blocking the thread.

Listing 15-3 begins creating the MainWindow class that inherits QWidget.
# file_rename_threading.py
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(600, 250)
        self.setWindowTitle("15.1 - Change File Names GUI")
        self.directory = ""
        self.combo_value = ""
        self.setUpMainWindow()
        self.show()
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = MainWindow()
    sys.exit(app.exec())
Listing 15-3

Base code for the MainWindow class

The variable directory is used to store the value of the directory selected, and combo_value pertains to the file extension value selected in the QComboBox.

In Listing 15-4, setUpMainWindow() is used to create the label, line edit, and button for selecting a directory. Various widgets in this program also use tooltips to provide more information to the user about a widget’s purpose or functionality.
# file_rename_threading.py
    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""
        dir_label = QLabel(
            """<p>Use Button to Choose Directory and
            Change File Names:</p>""")
        self.dir_edit = QLineEdit()
        dir_button = QPushButton("Select Directory")
        dir_button.setToolTip("Select file directory.")
        dir_button.clicked.connect(self.chooseDirectory)
Listing 15-4

Creating the setUpMainWindow() for the file renaming GUI, part 1

For specifying new file names, text can be entered into change_name_edit in Listing 15-5. The QComboBox is used to determine which file types to change in the selected directory. Only files with the selected extension will be changed while the renaming process is running.
# file_rename_threading.py
        self.change_name_edit = QLineEdit()
        self.change_name_edit.setToolTip(
            """<p>Files will be appended with numerical
            values. For example: filename<b>01</b>.jpg</p>""")
        self.change_name_edit.setPlaceholderText(
            "Change file names to…")
        file_exts = [".jpg", ".jpeg", ".png", ".gif", ".txt"]
        self.combo_value = file_exts[0]
        # Create combo box for selecting file extensions
        ext_combo = QComboBox()
        ext_combo.setToolTip(
            "Only files with this extension will be changed.")
        ext_combo.addItems(file_exts)
        ext_combo.currentTextChanged.connect(
            self.updateComboValue)
        rename_button = QPushButton("Rename Files")
        rename_button.setToolTip(
            "Begin renaming files in directory.")
        rename_button.clicked.connect(self.renameFiles)
Listing 15-5

Creating the setUpMainWindow() for the file renaming GUI, part 2

The rename_button instance is used to begin the process of renaming files. Clicking the button emits a signal that calls the renameFiles() slot.

Listing 15-6 finishes setting up the main window by creating the text edit and progress bar widgets that provide feedback about the renaming process. In addition, a QPushButton is created that will be enabled after rename_button is pressed and while files are being renamed.
# file_rename_threading.py
        # Text edit is for displaying the file names as they
        # are updated
        self.display_files_tedit = QTextEdit()
        self.display_files_tedit.setReadOnly(True)
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        self.stop_button = QPushButton("Stop")
        self.stop_button.setEnabled(False)
        # Create layout and arrange widgets
        grid = QGridLayout()
        grid.addWidget(dir_label, 0, 0)
        grid.addWidget(self.dir_edit, 1, 0, 1, 2)
        grid.addWidget(dir_button, 1, 2)
        grid.addWidget(self.change_name_edit, 2, 0)
        grid.addWidget(ext_combo, 2, 1)
        grid.addWidget(rename_button, 2, 2)
        grid.addWidget(self.display_files_tedit, 3, 0, 1, 3)
        grid.addWidget(self.progress_bar, 4, 0, 1, 2)
        grid.addWidget(self.stop_button, 4, 2)
        self.setLayout(grid)
Listing 15-6

Creating the setUpMainWindow() for the file renaming GUI, part 3

The widgets are then organized in a QGridLayout.

The chooseDirectory() slot in Listing 15-7 is called when dir_button is clicked and opens a QFileDialog for selecting directories. Once a directory is chosen, the user can enter the new file names into change_name_edit and select the file extension for the types of files to change in the combo box.
# file_rename_threading.py
    def chooseDirectory(self):
        """Choose file directory."""
        file_dialog = QFileDialog(self)
        file_dialog.setFileMode(
            QFileDialog.FileMode.Directory)
        self.directory = file_dialog.getExistingDirectory(
            self, "Open Directory", "",
            QFileDialog.Option.ShowDirsOnly)
        if self.directory:
            self.dir_edit.setText(self.directory)
            # Set the max value of progress bar equal to max
            # number of files in the directory
            num_of_files = len(
                [name for name in os.listdir(self.directory)])
            self.progress_bar.setRange(0, num_of_files)
Listing 15-7

Creating the chooseDirectory() slot

Directories in this application can only be selected by using the chooseDirectory() slot. We are also able to set the max range of the QProgressBar using the total number of files in the directory.

Renaming the files could take place in the main thread. This wouldn’t be a problem for a few files. However, if the user wants to work with a large number of files, this would cause the GUI to be locked until the operations are finished. Therefore, the process for renaming the files, along with updating the progress bar and the text edit widgets, is performed in the worker thread. An instance of the Worker class is created in Listing 15-8.
# file_rename_threading.py
    def renameFiles(self):
        """Create instance of worker thread to handle
        the file renaming process."""
        prefix_text = self.change_name_edit.text()
        if self.directory != "" and prefix_text != "":
            self.worker = Worker(
                self.directory, self.combo_value, prefix_text)
            self.worker.clear_text_edit_signal.connect(
                self.display_files_tedit.clear)
            self.stop_button.setEnabled(True)
            self.stop_button.repaint()
            self.stop_button.clicked.connect(
                self.worker.stopRunning)
            self.worker.update_value_signal.connect(
                self.updateProgressBar)
            self.worker.update_text_edit_signal.connect(
                self.updateTextEdit)
            self.worker.finished.connect(
                self.worker.deleteLater)
            self.worker.start()
Listing 15-8

Code for the renameFiles() slot that creates the worker thread

For Listing 15-9, directory, combo_value, and prefix_text are passed to the newly created worker thread. The worker_clear_text_signal is then connected to display_files_text instance’s clear() method.

If stop_button is clicked at this point, it will call the Worker class’s stopRunning() slot, causing the thread to end and resetting the progress bar and text edit. The other Worker signals are also connected to the slots in Listing 15-9.

QThread also has a finished signal that is emitted when the read stops running. The finished signal is connected to the QObject method deleteLater(), which is used to delete the worker object and release objects that were created while the thread was running.
# file_rename_threading.py
    def updateComboValue(self, text):
        """Change the combo box value. Values represent
        the different file extensions."""
        self.combo_value = text
        print(self.combo_value)
    def updateProgressBar(self, value):
        self.progress_bar.setValue(value)
    def updateTextEdit(self, old_text, new_text):
        self.display_files_tedit.append(
            f"[INFO] {old_text} changed to {new_text}.")
Listing 15-9

Code for the slots that update widget values

The updateProgressBar() and updateTextEdit() slots are connected to the worker thread’s signals.

You can now run the program and locate a local folder. If you find that the process is too fast and want to see the processes actually running, you can uncomment time.sleep(1.0) in the Worker class to slow down the process.

Summary

Preventing GUIs from becoming frozen while processing long operations is important for a user’s experience. There are a few options for effectively handling blocking in your application, including using timers and threads. Qt provides a class, QThread, that, combined with signals and slots, can be used for handling additional processes in GUI applications. However, you must be careful when using QThread to ensure that threads protect access to their own data. While not displayed in this chapter’s short project, QThread also has methods, such as started(), finished(), wait(), and quit(), for managing threads.

In Chapter 16, we will build multiple example projects to learn and practice a variety of concepts not covered in previous chapters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset