Vous êtes sur la page 1sur 19

25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

PyQt QThreadPool

Résumé : dans ce tutoriel, vous apprendrez à créer une application multithreading PyQt qui utilise
et classe. QThreadPool QRunnable

Introduction aux classes QThreadPool & QRunnable


La classe vous permet de décharger une tâche de longue durée vers un thread de travail pour
rendre l’application plus réactive. La classe QThread fonctionne correctement si l’application
comporte quelques threads de travail. QThread (https://www.pythontutorial.net/pyqt/pyqt-qthread/)

Un programme multithread est efficace lorsqu’il possède un nombre d’objets correspondant au


nombre de cœurs de processeur. QThread

En outre, la création de threads est assez coûteuse en termes de ressources informatiques. Par
conséquent, le programme doit réutiliser autant que possible les threads créés.

L’utilisation de la classe pour gérer les threads de travail présente donc deux défis principaux
: QThread

Déterminez le nombre idéal de threads pour l’application en fonction du nombre de cœurs de


processeur.

Réutilisez et recyclez les fils autant que possible.

Heureusement, PyQt a une classe qui résout ces défis pour vous. La classe est souvent utilisée avec
la classe. QThreadPool QThreadPool QRunnable

La classe représente une tâche que vous souhaitez exécuter dans un thread de
travail. QRunnable

Le exécute un objet, gère et recycle automatiquement les threads. QThreadPool QRunnable

Chaque application Qt possède un objet global accessible via la méthode statique de la


classe. QThreadPool globalInstance() QThreadPool

Pour utiliser les classes and, procédez comme suit : QThreadPool QRunnable

https://www.pythontutorial.net/pyqt/qthreadpool/ 1/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Tout d’abord, créez une classe qui hérite (https://www.pythontutorial.net/python-oop/python-inheritance/) de la


classe et remplacez la méthode : QRunnable run()

class Worker(QRunnable):
@Slot()
def run(self):
# place a long-running task here
pass

Ensuite, accédez au pool de threads à partir de la fenêtre principale et démarrez les threads de
travail :

class MainWindow(QMainWindow):
# other methods
# ...

def start(self):
""" Create and execute worker threads
"""
pool = QThreadPool.globalInstance()
for _ in range(1, 100):
pool.start(Worker())

Pour mettre à jour la progression du collaborateur vers le thread principal, vous utilisez des signaux
et des emplacements. Cependant, le ne prend pas en charge le signal. QRunnable

Par conséquent, vous devez définir une classe distincte qui hérite de la et utilise cette classe dans la
classe Worker. Voici les étapes : QObject

Tout d’abord, définissez la classe qui sous-classe la classe : Signals QObject

class Signals(QObject):
completed = Signal()

Dans la classe, nous définissons un signal appelé . Notez que vous pouvez définir autant de signaux
que nécessaire. Signals completed

https://www.pythontutorial.net/pyqt/qthreadpool/ 2/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Deuxièmement, émettez le signal lorsque le travail est terminé dans la classe: completed Worker

class Runnable(QRunnable):
def __init__(self):
super().__init__()
self.signals = Signals()

@Slot()
def run(self):
# long running task
# ...
# emit the completed signal
self.signals.completed.emit()

Troisièmement, connectez le signal du thread de production à un emplacement de la fenêtre


principale avant de soumettre le travailleur au pool :

class MainWindow(QMainWindow):
# other methods
# ...

def start(self):
""" Create and execute worker threads
"""
pool = QThreadPool.globalInstance()
for _ in range(1, 100):
worker = Worker()
worker.signals.completed.connect(self.update)
pool.start(worker)

def update(self):
# update the worker
pass

Exemple QThreadPool
https://www.pythontutorial.net/pyqt/qthreadpool/ 3/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

L’exemple suivant illustre l’utilisation de la classe and : QThreadPool QRunnable

import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QGridLayou
from PyQt6.QtCore import QRunnable, QObject, QThreadPool, pyqtSignal as Signal

class Signals(QObject):
started = Signal(int)
completed = Signal(int)

class Worker(QRunnable):
def __init__(self, n):
super().__init__()
self.n = n
self.signals = Signals()

@Slot()
def run(self):
self.signals.started.emit(self.n)
time.sleep(self.n*1.1)
self.signals.completed.emit(self.n)

class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)

self.setWindowTitle('QThreadPool Demo')

self.job_count = 10
self.comleted_jobs = []

widget = QWidget()
https://www.pythontutorial.net/pyqt/qthreadpool/ 4/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

widget.setLayout(QGridLayout())
self.setCentralWidget(widget)

self.btn_start = QPushButton('Start', clicked=self.start_jobs)


self.progress_bar = QProgressBar(minimum=0, maximum=self.job_count)
self.list = QListWidget()

widget.layout().addWidget(self.list, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)

self.show()

def start_jobs(self):
self.restart()
pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
worker = Worker(i)
worker.signals.completed.connect(self.complete)
worker.signals.started.connect(self.start)
pool.start(worker)

def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)

def start(self, n):


self.list.addItem(f'Job #{n} started...')

def complete(self, n):


self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))

if len(self.comleted_jobs) == self.job_count:

https://www.pythontutorial.net/pyqt/qthreadpool/ 5/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

self.btn_start.setEnabled(True)

if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())

def complete(self, n):


self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))

if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)

Sortie:

Classe Signals

https://www.pythontutorial.net/pyqt/qthreadpool/ 6/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Définissez la classe Signals qui hérite de la classe pour prendre en charge les signaux. Dans la
classe, nous définissons deux signaux : QObject Signals

Le signal sera émis lorsqu’un travailleur est démarré. started

Le signal sera émis lorsqu’un travailleur aura terminé. completed

Les deux signaux acceptent un entier qui identifie le numéro de tâche :

class Signals(QObject):
started = Signal(int)
completed = Signal(int)

Catégorie ouvrière

La classe hérite de la classe. La classe représente une tâche de longue durée que nous déchargeons
vers un thread de travail : Worker QRunnable Worker

class Worker(QRunnable):
def __init__(self, n):
super().__init__()
self.n = n
self.signals = Signals()

@Slot()
def run(self):
self.signals.started.emit(self.n)
time.sleep(self.n*1.1)
self.signals.completed.emit(self.n)

Tout d’abord, initialisez le numéro de tâche (n) et l’objet Signals dans la méthode. __init__()

Deuxièmement, remplacez la méthode de la classe. Pour simuler une tâche de longue durée, nous
utilisons la fonction du module de temps. Avant de démarrer la minuterie, nous émettons le signal
de démarrage; Une fois la minuterie terminée, nous émettons le signal
terminé. run() QRunnable sleep()

https://www.pythontutorial.net/pyqt/qthreadpool/ 7/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

MainWindow, classe

La classe définit le pour l’application : MainWindow UI

class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)

self.setWindowTitle('QThreadPool Demo')

self.comleted_jobs = []
self.job_count = 10

widget = QWidget()
widget.setLayout(QGridLayout())
self.setCentralWidget(widget)

self.btn_start = QPushButton('Start', clicked=self.start_jobs)


self.progress_bar = QProgressBar(minimum=0, maximum=self.job_count)
self.list = QListWidget()

widget.layout().addWidget(self.list, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)

self.show()

def start_jobs(self):
self.restart()

pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
runnable = Worker(i)
runnable.signals.completed.connect(self.complete)
runnable.signals.started.connect(self.start)
pool.start(runnable)

https://www.pythontutorial.net/pyqt/qthreadpool/ 8/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)

def start(self, n):


self.list.addItem(f'Job #{n} started...')

def complete(self, n):


self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))

if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)

Tout d’abord, initialisez le nombre de tâches () et listez dans la méthode de la classe


: job_count completed_jobs __init__() MainWindow

self.job_count = 10
self.comleted_jobs = []

Ensuite, définissez la méthode qui sera exécutée lorsque l’utilisateur cliquera sur le bouton
Démarrer : start_jobs()

def start_jobs(self):
self.restart()
pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
worker = Worker(i)
worker.signals.completed.connect(self.complete)
worker.signals.started.connect(self.start)
pool.start(worker)

https://www.pythontutorial.net/pyqt/qthreadpool/ 9/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Le réinitialise le , met à zéro la barre de progression et désactive le bouton de démarrage


: restart() completed_jobs

def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)

Pour obtenir l’objet, nous utilisons la classe : QThreadPool globalInstance() QThreadPool

pool = QThreadPool.globalInstance()

Nous créons un certain nombre de travailleurs, connectons leurs signaux aux méthodes de la classe
et démarrons des threads de travail à l’aide de la méthode de
l’objet. MainWindow start() QThreadPool

La méthode ajoute le message qui démarre un thread de travail au : start() QListWidget

def start(self, n):


self.list.addItem(f'Job #{n} started...')

La méthode s’exécute chaque fois qu’un thread de production est terminé. Il ajoute un message au
, met à jour la barre de progression et active le bouton Démarrer si tous les threads de travail sont
terminés : completed() QListWidget

def complete(self, n):


self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))

if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)

Utiliser QThreadPool pour obtenir les cours des actions

https://www.pythontutorial.net/pyqt/qthreadpool/ 10/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Le programme de cotation en bourse suivant lit les symboles boursiers du fichier et utilise pour
obtenir les prix des actions sur le site Web de Yahoo Finance: symbols.txt QThreadPool

Programme de cotation en bourse :

import sys
from pathlib import Path

from PyQt6.QtCore import QRunnable, Qt, QObject, QThreadPool, pyqtSignal as Si


from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget,
from PyQt6.QtGui import QIcon

from lxml import html


import requests

class Signals(QObject):
completed = Signal(dict)

https://www.pythontutorial.net/pyqt/qthreadpool/ 11/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

class Stock(QRunnable):
BASE_URL = 'https://finance.yahoo.com/quote/'

def __init__(self, symbol):


super().__init__()
self.symbol = symbol
self.signal = Signals()

@Slot()
def run(self):
stock_url = f'{self.BASE_URL}{self.symbol}'
response = requests.get(stock_url)
if response.status_code != 200:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'}
return

tree = html.fromstring(response.text)
price_text = tree.xpath(
'//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]
)

if not price_text:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'}
return

price = float(price_text[0].replace(',', ''))

self.signal.completed.emit({'symbol': self.symbol, 'price': price})

class Window(QMainWindow):
def __init__(self, filename, *args, **kwargs):
super().__init__(*args, **kwargs)

https://www.pythontutorial.net/pyqt/qthreadpool/ 12/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

self.symbols = self.read_symbols(filename)

self.results = []

self.setWindowTitle('Stock Listing')
self.setGeometry(100, 100, 400, 300)
self.setWindowIcon(QIcon('./assets/stock.png'))

widget = QWidget()
widget.setLayout(QGridLayout())
self.setCentralWidget(widget)

# set up button & progress bar


self.btn_start = QPushButton('Get Prices', clicked=self.get_prices)
self.progress_bar = QProgressBar(minimum=1, maximum=len(self.symbols))

# set up table widget


self.table = QTableWidget(widget)
self.table.setColumnCount(2)
self.table.setColumnWidth(0, 150)
self.table.setColumnWidth(1, 150)

self.table.setHorizontalHeaderLabels(['Symbol', 'Price'])

widget.layout().addWidget(self.table, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)

# show the window


self.show()

def read_symbols(self, filename):


"""
Read symbols from a file
"""
path = Path(filename)

https://www.pythontutorial.net/pyqt/qthreadpool/ 13/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

text = path.read_text()
return [symbol.strip() for symbol in text.split('\n')]

def reset_ui(self):
self.progress_bar.setValue(1)
self.table.setRowCount(0)

def get_prices(self):
# reset ui
self.reset_ui()

# start worker threads


pool = QThreadPool.globalInstance()
stocks = [Stock(symbol) for symbol in self.symbols]
for stock in stocks:
stock.signal.completed.connect(self.update)
pool.start(stock)

def update(self, data):


# add a row to the table
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(data['symbol']))
self.table.setItem(row, 1, QTableWidgetItem(str(data['price'])))

# update the progress bar


self.progress_bar.setValue(row + 1)

# sort the list by symbols once completed


if row == len(self.symbols) - 1:
self.table.sortItems(0, Qt.SortOrder.AscendingOrder)

if __name__ == '__main__':
app = QApplication(sys.argv)

https://www.pythontutorial.net/pyqt/qthreadpool/ 14/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

window = Window('symbols.txt')
sys.exit(app.exec())

Comment ça marche.

Classe Signals

Nous définissons la classe qui est une sous-classe du . La classe Signals a une variable de classe
terminée qui est une instance de la classe. Signals QObject Signal

Le signal terminé contient un dictionnaire et est émis une fois que le programme a terminé
d’obtenir le cours de l’action.

class Signals(QObject):
completed = Signal(dict)

Classe d’actions

La classe hérite de la classe. Il remplace la méthode qui obtient le cours de l’action sur le site Web
de Yahoo Finance. STock QRunnable run()

Une fois terminée, la méthode émet le signal terminé avec le symbole boursier et le prix. run()

Si une erreur se produit comme si le symbole est introuvable ou si le site Web modifie la façon
dont il affiche le cours de l’action, la méthode renvoie le symbole avec le prix sous forme de chaîne
N/A. run()

class Stock(QRunnable):
BASE_URL = 'https://finance.yahoo.com/quote/'

def __init__(self, symbol):


super().__init__()
self.symbol = symbol
self.signal = Signals()

@Slot()
def run(self):
https://www.pythontutorial.net/pyqt/qthreadpool/ 15/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

stock_url = f'{self.BASE_URL}{self.symbol}'
response = requests.get(stock_url)
if response.status_code != 200:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'}
return

tree = html.fromstring(response.text)
price_text = tree.xpath(
'//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]
)

if not price_text:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'}
return

price = float(price_text[0].replace(',', ''))

self.signal.completed.emit({'symbol': self.symbol, 'price': price})

Notez que Yahoo Finance peut modifier sa structure. Pour que le programme fonctionne, vous
devez changer le XPath du prix du nouveau:

//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]/text()

MainWindow, classe

Tout d’abord, lisez les symboles d’un fichier et affectez-les aux variables : self.symbols

self.symbols = self.read_symbols(filename)

La méthode ressemble à ceci: read_symbols()

def read_symbols(self, filename):


path = Path(filename)

https://www.pythontutorial.net/pyqt/qthreadpool/ 16/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

text = path.read_text()
return [symbol.strip() for symbol in text.split('\n')]

Le fichier texte () contient chaque symbole par ligne : symbols.txt

AAPL
MSFT
GOOG
AMZN
TSLA
META
NVDA
BABA
CRM
INTC
PYPL
AMD
ATVI
EA
TTD
ORCL

Deuxièmement, définissez le qui utilise le pour créer des threads de travail pour obtenir les cours
des actions: get_prices QThreadPool

def get_prices(self):
# reset ui
self.reset_ui()

# start worker threads


pool = QThreadPool.globalInstance()
stocks = [Stock(symbol) for symbol in self.symbols]
for stock in stocks:

https://www.pythontutorial.net/pyqt/qthreadpool/ 17/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

stock.signal.completed.connect(self.update)
pool.start(stock)

La méthode efface toutes les lignes de la et définit la barre de progression sur sa valeur minimale
: reset_ui() QTableWidget

def reset_ui(self):
self.table.setRowCount(0)
self.progress_bar.setValue(1)

Troisièmement, définissez la méthode qui sera appelée une fois chaque thread de travail terminé.
La méthode ajoute une nouvelle ligne à la table, met à jour la barre de progression et trie les
symboles une fois que tous les threads de production sont terminés : update() update()

def update(self, data):


# add a row to the table
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(data['symbol']))
self.table.setItem(row, 1, QTableWidgetItem(str(data['price'])))

# update the progress bar


self.progress_bar.setValue(row + 1)

# sort the list by symbols once completed


if row == len(self.symbols) - 1:
self.table.sortItems(0, Qt.SortOrder.AscendingOrder)

Résumé

Utilisez la classe pour représenter une tâche de longue durée qui sera déchargée sur un thread
de travail. QRunnable

Utilisez la classe pour gérer automatiquement les threads de production. QThreadPool

https://www.pythontutorial.net/pyqt/qthreadpool/ 18/19
25/04/2023 02:01 Multithreading PyQt avec QThreadPool & QRunnable

Chaque application PyQt a un objet. Utilisez la méthode pour obtenir l’objet


global. QThreadPool globalInstance() QThreadPool

Utilisez la méthode de l’objet pour démarrer un thread de travail. start() QThreadPool

https://www.pythontutorial.net/pyqt/qthreadpool/ 19/19

Vous aimerez peut-être aussi