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.
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.
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.
- 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.
With QTimer and signals and slots, you can schedule operations to be performed at certain intervals in the future.
- 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.
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.
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.
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.
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
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.
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.
Creating the setUpMainWindow() for the file renaming GUI, part 1
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.
Creating the setUpMainWindow() for the file renaming GUI, part 3
The widgets are then organized in a QGridLayout.
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.
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.
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.