Long-running threads
Using QThread
A simple thread
concurrent/qthread_1.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str) # <1>
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
counter = 0
while True:
time.sleep(0.1)
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
print("Thread complete")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start() # <2>
label = QLabel("Output will appear here")
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.setCentralWidget(label)
self.show()
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
Thread control
concurrent/qthread_2.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
counter = 0
while True:
time.sleep(0.1)
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
print("Thread complete")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
button = QPushButton("Kill thread")
# Terminate (kill immediately) the thread.
button.pressed.connect(self.thread.terminate)
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_2b.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
counter = 0
while True:
time.sleep(0.1)
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
button = QPushButton("Kill thread")
# Terminate (kill immediately) the thread.
button.pressed.connect(self.thread.terminate)
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished) # <1>
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def thread_has_finished(self):
print("Thread has finished.")
print(
self.thread,
self.thread.isRunning(),
self.thread.isFinished(),
) # <2>
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_2c.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
counter = 0
while True:
time.sleep(0.1)
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
if counter > 50:
return # <1>
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
button = QPushButton("Kill thread")
# Terminate (kill immediately) the thread.
button.pressed.connect(self.thread.terminate)
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def thread_has_finished(self):
print("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
Sending data
concurrent/qthread_3.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
self.data = None
self.is_running = True
print("Thread start")
counter = 0
while self.is_running:
time.sleep(0.1)
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
# tag::stop[]
button = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button.pressed.connect(self.thread.stop)
# end::stop[]
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
# tag::thread_has_finished[]
def thread_has_finished(self):
print("Thread has finished.")
print(
self.thread,
self.thread.isRunning(),
self.thread.isFinished(),
)
# end::thread_has_finished[]
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_4.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
self.data = None
self.is_running = True
print("Thread start")
counter = 0
while self.is_running:
while self.data is None:
time.sleep(0.1) # wait for data <1>.
# Output the number as a formatted string.
counter += self.data
self.result.emit(f"The cumulative total is {counter}")
self.data = None
def send_data(self, data):
"""
Receive data onto internal variable.
"""
self.data = data
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
self.numeric_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.numeric_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data(self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(self.numeric_input.value())
def thread_has_finished(self):
print("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_4b.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
# tag::is_running[]
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
self.data = None
self.is_running = True
counter = 0
while True:
while self.data is None:
if not self.is_running:
return # Exit thread.
time.sleep(0.1) # wait for data <1>.
# Output the number as a formatted string.
counter += self.data
self.result.emit(f"The cumulative total is {counter}")
self.data = None
# end::is_running[]
def send_data(self, data):
"""
Receive data onto internal variable.
"""
self.data = data
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
self.numeric_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.numeric_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data(self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(self.numeric_input.value())
def thread_has_finished(self):
print("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_5.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
def __init__(self, initial_data):
super().__init__()
self.data = initial_data
# end::initial_data[]
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
self.is_running = True
counter = 0
while True:
while self.data is None:
if not self.is_running:
return # Exit thread.
time.sleep(0.1) # wait for data <1>.
# Output the number as a formatted string.
counter += self.data
self.result.emit(f"The cumulative total is {counter}")
self.data = None
def send_data(self, data):
"""
Receive data onto internal variable.
"""
self.data = data
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread(500)
self.thread.start()
# ...
# end::mainwindow[]
self.numeric_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.numeric_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data(self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(self.numeric_input.value())
def thread_has_finished(self):
print("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_6.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
def __init__(self, initial_counter):
super().__init__()
self.counter = initial_counter
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
self.is_running = True
self.waiting_for_data = True
while True:
while self.waiting_for_data:
if not self.is_running:
return # Exit thread.
time.sleep(0.1) # wait for data <1>.
# Output the number as a formatted string.
self.counter += self.input_add
self.counter *= self.input_multiply
self.result.emit(f"The cumulative total is {self.counter}")
self.waiting_for_data = True
def send_data(self, add, multiply):
"""
Receive data onto internal variable.
"""
self.input_add = add
self.input_multiply = multiply
self.waiting_for_data = False
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread(500)
self.thread.start()
self.add_input = QSpinBox()
self.mult_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.add_input)
layout.addWidget(self.mult_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data(self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(
self.add_input.value(), self.mult_input.value()
)
def thread_has_finished(self):
print("Thread has finished.")
# end::mainwindow[]
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
concurrent/qthread_6b.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread(QThread):
# end::thread[]
"""
Worker thread
"""
result = pyqtSignal(str)
def __init__(self, initial_counter):
super().__init__()
self.counter = initial_counter
@pyqtSlot()
def run(self):
"""
Your code goes in this method
"""
print("Thread start")
self.is_running = True
self.waiting_for_data = True
while True:
while self.waiting_for_data:
if not self.is_running:
return # Exit thread.
time.sleep(0.1) # wait for data <1>.
# Output the number as a formatted string.
self.counter += self.input_add
self.counter *= self.input_multiply
self.result.emit(f"The cumulative total is {self.counter}")
self.waiting_for_data = True
# tag::data_methods[]
def send_add(self, add):
self.input_add = add
def send_multiply(self, multiply):
self.input_multiply = multiply
def calculate(self):
self.waiting_for_data = False # Release the lock & calculate.
# end::data_methods[]
def stop(self):
self.is_running = False
class MainWindow(QMainWindow):
# end::mainwindow[]
def __init__(self):
super().__init__()
# Create thread and start it.
self.thread = Thread(500)
self.thread.start()
self.add_input = QSpinBox()
self.mult_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.add_input)
layout.addWidget(self.mult_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
# tag::submit_data[]
def submit_data(self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_add(self.add_input.value())
self.thread.send_multiply(self.mult_input.value())
self.thread.calculate()
# end::submit_data[]
def thread_has_finished(self):
print("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
27. Long-running threads
In the examples we’ve looked at so far we’ve been using QRunnable objects to execute tasks using the QThreadPool. The tasks we submitted were handled in order by the thread pool, with the maximum concurrency constrained by the pool.
But what if you want something to execute right now regardless of what else is happening? Or, perhaps you want to keep a thread running in the background the entire time your application is running — to interact with some remote service or hardware, or to stream data through for processing. In that case the thread pool architecture may not be appropriate.
In this chapter we’ll look at PyQt6’s persistent thread interface QThread. It provides a very similar interface to the QRunnable objects you’ve already seen, but gives you complete control over when and how the thread is run.
Using QThread
Just like in the QRunnable examples, the QThread class acts as a wrapper around the code you want to execute in another thread. It handles the start up and shifting of the work to a separate thread, as well as managing and shutting down the thread once it completes. You just need to provide the code to execute. This is done by subclassing QThread and implementing a run() method.
A simple thread
Let’s start with a simple example. Below, we’ve implemented a worker thread which can perform arithmetic for us. We have added a single signal to the thread which we can use to send data out of the thread.
Listing 205. concurrent/qthread_1.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow
class Thread (QThread): """ Worker thread """
result = pyqtSignal(str) ①
@pyqtSlot() def run (self): """ Your code goes in this method """ print ("Thread start") counter = 0 while True: time.sleep( 0.1 )
Output the number as a formatted string.
self.result.emit(f"The number is {counter}") counter += 1 print ("Thread complete")
class MainWindow (QMainWindow): def init (self): super().init()
Create thread and start it.
self.thread = Thread() self.thread.start() ②
label = QLabel("Output will appear here")
Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.setCentralWidget(label) self.show()
app = QApplication(sys.argv)
window = MainWindow()
app. exec ()
①Unlike QRunnable the QThread class does inherit from QObject so we can define
the signals on the thread object itself.
②Call .start() to start the thread, not .run()!
If you run this example you’ll see a number in a window counting upwards. Not very exciting! But this counting is happening on a separate thread from your GUI and the result is being emitted using signals. This means the GUI isn’t blocked by the work taking place (although normal Python GIL rules apply).
Figure 211. QThread counter with the result displayed via a signal.
Try increasing the duration of the sleep() call and you’ll see that, even with the thread blocked the main GUI continues to run as normal.
ë
If you usually work with numpy or other libraries, experiment
with using them to perform more complex calculations in the
thread.
Z
You will usually want to add signals of some kind to your thread
for communication.
Thread control
Now we can start our thread, but we have no way to stop it. Unlike QRunnable the QThread class has a built-in method .terminate() which can be used to immediately kill a running thread. This is not a clean shutdown — the thread will simply stop wherever it was, and no Python exception will be thrown.
Listing 206. concurrent/qthread_2.py
class MainWindow (QMainWindow):
def __init__ (self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
button = QPushButton("Kill thread")
# Terminate (kill immediately) the thread.
button.pressed.connect(self.thread.terminate)
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
If you run this, you’ll notice that the "Thread complete" message we added after the thread’s main loop is never displayed. That’s because when we call .terminate() the execution just halts and never reaches that point in the code.
Figure 212. The thread can be terminated using the button control.
However, QThread has a finished signal which can be used to trigger some action after the thread completes. This is always fired — whether the thread terminates or shuts down cleanly.
The thread object persists after the thread has completed running & you can usually use this to query the thread for status. However be careful — if the thread was terminated, interacting with the thread object may cause your application to crash. The example below demonstrates this by attempting to print some information about the thread object after it is terminated.
Listing 207. concurrent/qthread_2b.py
class MainWindow (QMainWindow):
def __init__ (self):
super().__init__()
# Create thread and start it.
self.thread = Thread()
self.thread.start()
label = QLabel("Output will appear here")
button = QPushButton("Kill thread")
# Terminate (kill immediately) the thread.
button.pressed.connect(self.thread.terminate)
# Connect signal, so output appears on label.
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished) ①
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def thread_has_finished (self):
print ("Thread has finished.")
print (
self.thread,
self.thread.isRunning(),
self.thread.isFinished(),
) ②
①Connecting the finished signal to our custom slot.
②If you terminate the thread your application will likely crash here.
While you can terminate a thread from inside, it’s cleaner to just return from the run() method. Once you exit the run() method the thread will be automatically
ended and cleaned up safely & the finished signal will fire.
Listing 208. concurrent/qthread_2c.py
class Thread (QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run (self):
"""
Your code goes in this method
"""
print ("Thread start")
counter = 0
while True:
time.sleep( 0.1 )
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
if counter > 50 :
return ①
①calling return in the run() method will exit and end the thread.
When you run the above example, the counter will stop at 50 because we return from the run() method. If you try and press the terminate button after this has happened, notice that you don’t receive the thread finished signal a second time — the thread has already been shut down, so it cannot be terminated.
Sending data
In the previous example our thread was running but not able to receive any data from outside. Usually when you use long-running threads you want to be able to communicate with them, either to pass them work, or to control their behavior in some other way.
We’ve been talking about how important it is to shut your threads down cleanly. So let’s start by looking at how we can communicate with our thread that we want it to shut down. As with the QRunnable examples, we can do this by using an internal flag in the thread to control the main loop , with the loop continuing while our flag is True.
To shut the thread down, we change the value of this flag. Below we’ve implemented this using a flag named is_running and custom method .stop() on the thread. When called, this method toggles the is_running flag to False. With the flag set to False the main loop will end, the thread will exit the run() method and the thread will shut down.
Listing 209. concurrent/qthread_3.py
class Thread (QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run (self):
"""
Your code goes in this method
"""
self.data = None
self.is_running = True
print ("Thread start")
counter = 0
while self.is_running:
time.sleep( 0.1 )
# Output the number as a formatted string.
self.result.emit(f"The number is {counter}")
counter += 1
def stop (self):
self.is_running = False
We can then modify our button to call the custom stop() method, rather than terminate.
Listing 210. concurrent/qthread_3.py
button = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button.pressed.connect(self.thread.stop)
Since the thread shutdown cleanly, we can access the thread object without risk of it crashing. Re-add the print statement to the thread_has_finished method.
Listing 211. concurrent/qthread_3.py
def thread_has_finished (self):
print ("Thread has finished.")
print (
self.thread,
self.thread.isRunning(),
self.thread.isFinished(),
)
If you run this you will see the number counting up as before, but pressing the button will stop the thread dead. Notice that we are able to display the metadata about the thread after the shutdown, because the thread didn’t crash.
Figure 213. The thread can now be cleanly shutdown using the button.
We can use this same general approach to send any data into the thread that we
like. Below we’ve extended our custom Thread class to add a send_data method, which accepts a single argument, and stores it internally on the thread via self.
Using this we can send in data which is accessible within the threads run() method and use it to modify behavior.
Listing 212. concurrent/qthread_4.py
import sys
import time
from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
class Thread (QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
@pyqtSlot()
def run (self):
"""
Your code goes in this method
"""
self.data = None
self.is_running = True
print ("Thread start")
counter = 0
while self.is_running:
while self.data is None:
time.sleep( 0.1 ) # wait for data <1>.
Output the number as a formatted string.
counter += self.data self.result.emit(f"The cumulative total is {counter}") self.data = None
def send_data (self, data): """ Receive data onto internal variable. """ self.data = data
def stop (self): self.is_running = False
class MainWindow (QMainWindow): def init (self): super().init()
Create thread and start it.
self.thread = Thread() self.thread.start()
self.numeric_input = QSpinBox() button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data) self.thread.result.connect(label.setText) self.thread.finished.connect(self.thread_has_finished)
container = QWidget() layout = QVBoxLayout() layout.addWidget(self.numeric_input) layout.addWidget(button_input) layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data (self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(self.numeric_input.value())
def thread_has_finished (self):
print ("Thread has finished.")
app = QApplication(sys.argv)
window = MainWindow()
app. exec ()
If you run this example you’ll see the following window. Use the QSpinBox to select a number and then press the button to submit it to the thread. The thread will add the incoming number onto the current counter and return the result.
Figure 214. We can now submit data to our thread, using the QSpinBox and button.
If you use the Shutdown thread button to stop the thread you may notice something a little strange. The thread does shutdown, but you can submit one more number before it does so and the calculation is still performed — try it! This
is because the is_running check is performed at the top of the loop and then the thread waits for input.
To fix this, we need to move the check of the is_running flag into the waiting loop.
Listing 213. concurrent/qthread_4b.py
@pyqtSlot()
def run (self):
"""
Your code goes in this method
"""
print ("Thread start")
self.data = None
self.is_running = True
counter = 0
while True:
while self.data is None:
if not self.is_running:
return # Exit thread.
time.sleep( 0.1 ) # wait for data <1>.
# Output the number as a formatted string.
counter += self.data
self.result.emit(f"The cumulative total is {counter}")
self.data = None
If you run the example now, you’ll see that if the button is pressed while the thread is waiting, it will exit immediately.
j
Be careful when placing thread exit control conditions in your
threads, to avoid any unexpected side-effects. Try and check
before performing any new tasks/calculations and before
emitting any data.
Often you’ll also want to pass in some initial state data, for example configuration options to control the subsequent running of the thread. We can pass that in just as we did for QRunnable by adding arguments to our init block. The provided
arguments must be stored on the self object to be available in the run() method.
Listing 214. concurrent/qthread_5.py
class Thread (QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
def __init__ (self, initial_data):
super().__init__()
self.data = initial_data
class MainWindow (QMainWindow):
def __init__ (self):
super().__init__()
# Create thread and start it.
self.thread = Thread( 500 )
self.thread.start()
# ...
Using these two approaches you can provide any data you need to your thread. This pattern of waiting for data in the thread (using sleep loop), processing the data and returning it via a signal is the most common pattern when working with long-running threads in Qt applications.
Let’s extend the example one more time to demonstrate passing in multiple data types. In this example we modify our thread to use an explicit lock, called waiting_for_data which we can toggle between True and False. You can use this
Listing 215. concurrent/qthread_6.py
class Thread (QThread):
"""
Worker thread
"""
result = pyqtSignal(str)
def init (self, initial_counter): super().init() self.counter = initial_counter
@pyqtSlot() def run (self): """ Your code goes in this method """ print ("Thread start") self.is_running = True self.waiting_for_data = True while True: while self.waiting_for_data: if not self.is_running: return # Exit thread. time.sleep( 0.1 ) # wait for data <1>.
Output the number as a formatted string.
self.counter += self.input_add self.counter *= self.input_multiply self.result.emit(f"The cumulative total is {self.counter} ") self.waiting_for_data = True
def send_data (self, add, multiply): """ Receive data onto internal variable. """ self.input_add = add self.input_multiply = multiply self.waiting_for_data = False
def stop (self): self.is_running = False
class MainWindow (QMainWindow): def init (self): super().init()
Create thread and start it.
self.thread = Thread( 500 )
self.thread.start()
self.add_input = QSpinBox()
self.mult_input = QSpinBox()
button_input = QPushButton("Submit number")
label = QLabel("Output will appear here")
button_stop = QPushButton("Shutdown thread")
# Shutdown the thread nicely.
button_stop.pressed.connect(self.thread.stop)
# Connect signal, so output appears on label.
button_input.pressed.connect(self.submit_data)
self.thread.result.connect(label.setText)
self.thread.finished.connect(self.thread_has_finished)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.add_input)
layout.addWidget(self.mult_input)
layout.addWidget(button_input)
layout.addWidget(label)
layout.addWidget(button_stop)
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
def submit_data (self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_data(
self.add_input.value(), self.mult_input.value()
)
def thread_has_finished (self):
print ("Thread has finished.")
You can also split your submit data methods out into a separate method per value and implement an explicit calculate method which releases the lock. This
approach is well suited when you don’t necessarily want to update all values all of the time. For example, if you are reading data in from an external service or hardware.
Listing 216. concurrent/qthread_6b.py
class Thread (QThread):
def send_add (self, add):
self.input_add = add
def send_multiply (self, multiply):
self.input_multiply = multiply
def calculate (self):
self.waiting_for_data = False # Release the lock & calculate.
class MainWindow (QMainWindow):
def submit_data (self):
# Submit the value in the numeric_input widget to the thread.
self.thread.send_add(self.add_input.value())
self.thread.send_multiply(self.mult_input.value())
self.thread.calculate()
If you run this example you’ll see exactly the same behavior as before. Which approach makes the most sense in your application will depend on what the particular thread is doing.
ë
Don’t be afraid to mix and match the various threading
techniques you’ve learned. For example, in some applications it
will make sense to run certain parts of the application using
persistent threads and others using thread pools.
When a user does something in your application, the consequences of that action should be immediately apparent — either through the result of the action itself, or through an indication that something is being done that will provide the result. This is particularly important for long-running tasks, such as calculations or network requests, where a lack of feedback could prompt the user to repeatedly mash buttons and see nothing in return.
One simple approach is to disable buttons once an operation has been triggered. But with no other indicator this looks a lot like broken. A better alternative is to update the button with a "Working" message and an active progress indicator such as a spinner nearby. Progress bars are a common way to address this by informing the user of what is going on — and how long it’s going to take. But don’t fall into the trap of thinking progress bars are always useful! They should only be used when you they can present a linear progress towards a task
— if they don’t they can be more frustrating than having no Information at all. Any of these behaviors can give users the sense that something isn’t right leading to frustration to confusion — "what was that dialog I missed?!" These aren’t good things to make your users feel, so you should avoid it wherever possible.
Remember that your users don’t know what’s going on inside your application — their only insight is through the data you give them. Share data which is helpful to your users and keep everything else hidden. If you need debugging output, you can put it behind a menu.
DO Provide progress bars for long-running tasks.
DO Provide granular detail of sub-tasks where appropriate.
DO Estimate how long something will take, when you can.
DON’T Assume your users know which tasks are long or short.
DON’T Use progress bars that move up & down, or irregularly.
A Sense of Progress
Progress bars are not helpful if —
- they go backwards and forward
- they don’t increase linearly with progress
- they complete too quickly
Spinners or waiting icons can beused when the the duration of the
task Is unknown, or very short.
applications can haveSome complex
multiple concurrenttasks
'PyQt5_' 카테고리의 다른 글
Plotting with PyQtGraph (0) | 2023.03.13 |
---|---|
Running external commands & processes (0) | 2023.03.13 |
QRunnable examples (0) | 2023.03.13 |
Using the thread pool (0) | 2023.03.13 |
Using Custom Widgets in Qt Designer (0) | 2023.03.13 |
댓글