Formation Python
Les applications d'interface utilisateur graphique (GUI) PyQt ont fil conducteur d'exécution qui exécute la boucle d'événements et l'interface graphique. Si vous lancez un tâche de longue durée dans ce fil, votre GUI se figera jusqu'à ce que la tâche se termine. Pendant ce temps, l'utilisateur ne pourra pas interagir avec l'application, ce qui entraînera une mauvaise expérience utilisateur. Heureusement, PyQt est QThread
class vous permet de contourner ce problème.
Dans ce didacticiel, vous apprendrez à:
- Utilisez PyQt
QThread
pour éviter de geler les interfaces graphiques - Créer fils réutilisables avec
QThreadPool
etQRunnable
- Gérer communication inter-thread en utilisant des signaux et des slots
- Utiliser en toute sécurité ressources partagées avec les serrures de PyQt
- Utilisation les meilleures pratiques pour développer des applications GUI avec la prise en charge des threads de PyQt
Pour mieux comprendre comment utiliser les threads de PyQt, une connaissance préalable de la programmation GUI avec PyQt et la programmation multithread Python serait utile.
Gel d'une interface graphique avec des tâches de longue durée
Les tâches de longue durée occupant le thread principal d'une application GUI et provoquant le gel de l'application sont un problème courant dans la programmation GUI qui entraîne presque toujours une mauvaise expérience utilisateur. Par exemple, considérez l'application GUI suivante:
Dites que vous avez besoin du Compte libellé pour refléter le nombre total de clics sur le Clique moi! bouton. En cliquant sur le Tâche de longue haleine! Le bouton lancera une tâche qui prend beaucoup de temps à terminer. Votre tâche de longue durée peut être un téléchargement de fichier, une requête vers une base de données volumineuse ou toute autre opération gourmande en ressources.
Voici une première approche pour coder cette application à l'aide de PyQt et d'un seul thread d'exécution:
importer sys
de temps importer dormir
de PyQt5.QtCore importer Qt
de PyQt5.QtWidgets importer (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
classe La fenêtre(QMainWindow):
def __init__(soi, parent=Aucun):
super().__init__(parent)
soi.clicksCount = 0
soi.setupUi()
def setupUi(soi):
soi.setWindowTitle("Gel de l'interface graphique")
soi.redimensionner(300, 150)
soi.centralWidget = QWidget()
soi.setCentralWidget(soi.centralWidget)
# Créer et connecter des widgets
soi.clicksLabel = QLabel("Comptage: 0 clic", soi)
soi.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
soi.stepLabel = QLabel("Étape longue: 0")
soi.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
soi.countBtn = QPushButton("Cliquez sur moi!", soi)
soi.countBtn.cliqué.relier(soi.countClicks)
soi.longRunningBtn = QPushButton("Tâche de longue durée!", soi)
soi.longRunningBtn.cliqué.relier(soi.runLongTask)
# Définir la mise en page
disposition = QVBoxLayout()
disposition.addWidget(soi.clicksLabel)
disposition.addWidget(soi.countBtn)
disposition.addStretch()
disposition.addWidget(soi.stepLabel)
disposition.addWidget(soi.longRunningBtn)
soi.centralWidget.setLayout(disposition)
def countClicks(soi):
soi.clicksCount + = 1
soi.clicksLabel.Définir le texte(F"Compte: soi.clicksCount clics ")
def reportProgress(soi, n):
soi.stepLabel.Définir le texte(F"Étape longue: n")
def runLongTask(soi):
"" "Tâche de longue durée en 5 étapes." ""
pour je dans intervalle(5):
dormir(1)
soi.reportProgress(je + 1)
app = QApplication(sys.argv)
gagner = La fenêtre()
gagner.spectacle()
sys.sortie(app.exec())
Dans cette application d'interface graphique Freezing, .setupUi ()
crée tous les composants graphiques requis pour l'interface graphique. Un clic sur le Clique moi! appels de bouton .countClicks ()
, ce qui rend le texte du Compte L'étiquette reflète le nombre de clics sur les boutons.
Remarque: PyQt a d'abord été développé pour cibler Python 2, qui a un exec
mot-clé. Pour éviter un conflit de nom sur ces versions antérieures de PyQt, un trait de soulignement a été ajouté à la fin de .exec_ ()
.
Même si PyQt5 cible uniquement Python 3, qui n’a pas exec
mot-clé, la bibliothèque propose deux méthodes pour démarrer la boucle d'événements d'une application:
.exec_ ()
.exec ()
Les deux variantes de la méthode fonctionnent de la même manière, vous pouvez donc utiliser l'une ou l'autre dans vos applications.
En cliquant sur le Tâche de longue haleine! appels de bouton .runLongTask ()
, qui effectue une tâche qui prend 5
secondes pour terminer. Il s'agit d'une tâche hypothétique que vous avez codée en utilisant time.sleep (secondes)
, qui suspend l'exécution du thread appelant pendant le nombre de secondes donné, secondes
.
Dans .runLongTask ()
, tu appelles aussi .reportProgress ()
pour faire le Étape longue L'étiquette reflète la progression de l'opération.
Cette application fonctionne-t-elle comme vous l'entendez? Exécutez l'application et vérifiez son comportement:
Lorsque vous cliquez sur le Clique moi! bouton, l'étiquette indique le nombre de clics. Cependant, si vous cliquez sur le Tâche de longue haleine! , l'application se fige et ne répond plus. Les boutons ne répondent plus aux clics et les libellés ne reflètent pas l'état de l'application.
Au bout de cinq secondes, l'interface graphique de l'application est à nouveau mise à jour. le Compte L'étiquette affiche dix clics, reflétant cinq clics survenus pendant que l'interface graphique était figée. le Étape longue L'étiquette ne reflète pas la progression de votre opération de longue durée. Il saute de zéro à cinq sans montrer les étapes intermédiaires.
Remarque: Même si l’interface graphique de votre application se fige pendant la tâche de longue durée, l’application enregistre toujours les événements tels que les clics et les frappes au clavier. Il est tout simplement impossible de les traiter tant que le thread principal n’a pas été libéré.
L'interface graphique de l'application se fige en raison d'un thread principal bloqué. Le thread principal est occupé à traiter une tâche de longue durée et ne répond pas immédiatement aux actions de l'utilisateur. Il s’agit d’un comportement ennuyeux, car l’utilisateur ne sait pas avec certitude si l’application fonctionne correctement ou si elle est en panne.
Heureusement, il existe certaines techniques que vous pouvez utiliser pour contourner ce problème. Une solution couramment utilisée consiste à exécuter votre tâche de longue durée en dehors du thread principal de l'application à l'aide d'un fil de travail.
Dans les sections ci-dessous, vous apprendrez à utiliser la prise en charge intégrée des threads de PyQt pour résoudre le problème des interfaces graphiques qui ne répondent pas ou figées et fournir la meilleure expérience utilisateur possible dans vos applications.
Multithreading: les bases
Parfois, vous pouvez diviser vos programmes en plusieurs sous-programmes, ou Tâches, que vous pouvez exécuter dans plusieurs threads. Cela peut accélérer vos programmes ou vous aider à améliorer l'expérience utilisateur en empêchant vos programmes de se figer lors de l'exécution de tâches de longue durée.
UNE fil est un flux d'exécution distinct. Dans la plupart des systèmes d'exploitation, un thread est un composant d'un processus et les processus peuvent avoir plusieurs threads s'exécutant simultanément. Chaque processus représente une instance d'un programme ou d'une application en cours d'exécution dans un système informatique donné.
Vous pouvez avoir autant de threads que nécessaire. Le défi consiste à déterminer le bon nombre de threads à utiliser. Si vous travaillez avec des threads liés aux E / S, le nombre de threads sera limité par vos ressources système disponibles. D'un autre côté, si vous travaillez avec des threads liés au processeur, vous bénéficierez d'un nombre de threads égal ou inférieur au nombre de cœurs de processeur de votre système.
La création de programmes capables d'exécuter plusieurs tâches à l'aide de différents threads est une technique de programmation appelée programmation multithread. Idéalement, avec cette technique, plusieurs tâches s'exécutent indépendamment en même temps. Cependant, ce n’est pas toujours possible. Il existe au moins deux éléments qui peuvent empêcher un programme d'exécuter plusieurs threads en parallèle:
- L'unité centrale de traitement (CPU)
- Le langage de programmation
Par exemple, si vous avez une machine à processeur monocœur, vous ne pouvez pas exécuter plusieurs threads en même temps. Cependant, certains processeurs monocœur peuvent simuler l'exécution de threads parallèles en permettant au système d'exploitation de planifier le temps de traitement entre plusieurs threads. Cela donne l'impression que vos threads s'exécutent en parallèle même s'ils s'exécutent vraiment un à la fois.
D'un autre côté, si vous avez une machine à processeur multicœur ou un cluster d'ordinateurs, vous pourrez peut-être exécuter plusieurs threads en même temps. Dans ce cas, votre langage de programmation devient un facteur important.
Certains langages de programmation ont des composants internes qui interdisent en fait l'exécution réelle de plusieurs threads en parallèle. Dans ces cas, les threads semblent simplement s'exécuter en parallèle car ils tirent parti du système de planification des tâches.
Les programmes multithreads sont généralement plus difficiles à écrire, à maintenir et à déboguer que les programmes à thread unique en raison de la complexité liée au partage des ressources entre les threads, à la synchronisation de l'accès aux données et à la coordination de l'exécution des threads. Cela peut causer plusieurs problèmes:
-
La condition de concurrence est lorsque le comportement de l'application devient non déterministe en raison de l'ordre imprévisible des événements. C'est souvent le résultat de deux ou plusieurs threads accédant à une ressource partagée sans synchronisation appropriée. Par exemple, la lecture et l'écriture de mémoire à partir de différents threads peuvent conduire à une condition de concurrence si les opérations de lecture et d'écriture sont effectuées dans le mauvais ordre.
-
Un blocage se produit lorsque les threads attendent indéfiniment la libération d'une ressource verrouillée. Par exemple, si un thread verrouille une ressource et ne la déverrouille pas après son utilisation, les autres threads ne pourront pas utiliser cette ressource et attendront indéfiniment. Des blocages peuvent également se produire si le thread A attend que le thread B déverrouille une ressource et que le thread B attend que le thread A déverrouille une ressource différente. Les deux threads finiront par attendre éternellement.
-
Livelock est une situation dans laquelle deux ou plusieurs fils agissent de manière répétée en réponse aux actions de chacun. Les threads livelock ne peuvent pas progresser davantage sur leur tâche spécifique car ils sont trop occupés à se répondre. Cependant, ils ne sont ni bloqués ni morts.
-
La famine se produit lorsqu'un processus n'a jamais accès aux ressources dont il a besoin pour terminer son travail. Par exemple, si vous avez un processus qui ne peut pas accéder au temps CPU, alors le processus manque de temps CPU et ne peut pas faire son travail.
Lors de la création d'applications multithread, vous devez veiller à protéger vos ressources contre l'écriture simultanée ou l'accès à la modification d'état. En d'autres termes, vous devez empêcher plusieurs threads d'accéder à une ressource donnée en même temps.
Un large éventail d'applications peut bénéficier de l'utilisation de la programmation multithread d'au moins trois manières:
- Rendre vos applications plus rapides en tirant parti des processeurs multicœurs
- Simplifier la structure de l'application en la divisant en sous-tâches plus petites
- Garder votre application réactive et à jour en déchargeant les tâches de longue durée sur les threads de travail
Dans l'implémentation C de Python, également appelée CPython, les threads ne fonctionnent pas en parallèle. CPython a un verrou d'interpréteur global (GIL), qui est un verrou qui permet essentiellement à un seul thread Python de s'exécuter à la fois.
Cela peut affecter négativement les performances des applications Python threadées en raison de la surcharge résultant du changement de contexte entre les threads. Cependant, le multithreading en Python peut vous aider à résoudre le problème du gel ou du non-réponse des applications lors du traitement de tâches de longue durée.
Multithreading dans PyQt avec QThread
Qt, et donc PyQt, fournit sa propre infrastructure pour créer des applications multithread en utilisant QThread
. Les applications PyQt peuvent avoir deux types de threads différents:
- Fil principal
- Fils de travail
Le thread principal de l'application existe toujours. C'est là que s'exécutent l'application et son interface graphique. D'autre part, l'existence de threads de travail dépend des besoins de traitement de l’application. Par exemple, si votre application exécute généralement des tâches lourdes qui prennent beaucoup de temps à terminer, vous voudrez peut-être avoir des threads de travail pour exécuter ces tâches et éviter de geler l'interface graphique de l'application.
Le fil conducteur
Dans les applications PyQt, le principal thread d'exécution est également appelé Fil GUI car il gère tous les widgets et autres composants de l'interface graphique. Vous démarrez ce fil en appelant .exec ()
Sur ton QApplication
objet. Le thread principal exécute la boucle d'événements de l'application ainsi que votre code Python. Il gère également vos fenêtres, vos boîtes de dialogue et vos communications avec le système d'exploitation hôte.
Par défaut, tout événement ou tâche qui a lieu dans le thread principal de l'application, y compris les événements de l'utilisateur sur l'interface graphique elle-même, s'exécutera synchrone, ou une tâche après l'autre. Ainsi, si vous démarrez une tâche de longue durée dans le thread principal, l'application doit attendre la fin de cette tâche et l'interface graphique ne répond plus.
Il est important de noter que vous devez créer et mettre à jour tous vos widgets dans le fil de l’interface graphique. Cependant, vous pouvez exécuter d'autres tâches de longue durée dans les threads de travail et utiliser leurs résultats pour alimenter les composants GUI de votre application. Cela signifie que les composants GUI agiront comme des consommateurs qui reçoivent des informations des threads effectuant le travail réel.
Fils de travail
Vous pouvez créer autant de threads de travail que nécessaire dans vos applications PyQt. Les threads de travail sont des threads d'exécution secondaires que vous pouvez utiliser pour décharger les tâches de longue durée du thread principal et empêcher le gel de l'interface graphique.
Vous pouvez créer des threads de travail en utilisant QThread
. Chaque thread de travail peut avoir sa propre boucle d’événements et prendre en charge le mécanisme des signaux et des emplacements de PyQt pour communiquer avec le thread principal. Si vous créez un objet à partir d'une classe qui hérite de QObject
dans un thread particulier, alors cet objet est dit appartenir à, ou avoir un affinité avec, ce fil. Ses enfants doivent également appartenir au même fil.
QThread
n'est pas un fil en soi. C'est un wrapper autour d'un thread du système d'exploitation. Le vrai objet thread est créé lorsque vous appelez QThread.start ()
.
QThread
fournit une interface de programmation d'application (API) de haut niveau pour gérer les threads. Cette API comprend des signaux, tels que .commencé()
et .fini()
, qui sont émis lorsque le thread démarre et se termine. Il comprend également des méthodes et des emplacements, tels que .début()
, .attendez()
, .sortie()
, .quitter()
, .est fini()
, et .est en cours d'exécution()
.
Comme avec toutes les autres solutions de filetage, avec QThread
vous devez protéger vos données et ressources de concurrent, ou accès simultané. Sinon, vous serez confronté à de nombreux problèmes, notamment des blocages, une corruption de données, etc.
En utilisant QThread
vs Python filetage
Quand il s'agit de travailler avec des threads en Python, vous constaterez que la bibliothèque standard Python offre une solution cohérente et robuste avec le filetage
module. Ce module fournit une API de haut niveau pour faire de la programmation multithread en Python.
Normalement, vous utiliserez filetage
dans vos applications Python. Cependant, si vous utilisez PyQt pour créer des applications GUI avec Python, vous avez une autre option. PyQt fournit une API complète, entièrement intégrée et de haut niveau pour le multithreading.
Vous vous demandez peut-être, que dois-je utiliser dans mes applications PyQt, la prise en charge des threads de Python ou la prise en charge des threads de PyQt? La réponse est que cela dépend.
Par exemple, si vous créez une application GUI qui aura également une version Web, les threads de Python peuvent avoir plus de sens car votre back-end ne dépendra pas du tout de PyQt. Cependant, si vous créez des applications PyQt nues, les threads de PyQt sont faits pour vous.
L'utilisation de la prise en charge des threads de PyQt offre les avantages suivants:
- Classes liées aux threads sont entièrement intégrés au reste de l'infrastructure PyQt.
- Fils de travail peuvent avoir leur propre boucle d'événements, ce qui permet la gestion des événements.
- Communication inter-thread est possible en utilisant des signaux et des slots.
Une règle d'or peut être d'utiliser la prise en charge des threads de PyQt si vous allez interagir avec le reste de la bibliothèque, et d'utiliser la prise en charge des threads de Python dans le cas contraire.
En utilisant QThread
pour empêcher le gel des interfaces graphiques
Une utilisation courante des threads dans une application GUI est de décharger les tâches de longue durée sur les threads de travail afin que l’interface utilisateur reste sensible aux interactions de l’utilisateur. Dans PyQt, vous utilisez QThread
pour créer et gérer des threads de travail.
Selon la documentation de Qt, il existe deux façons principales de créer des threads de travail avec QThread
:
- Instancier
QThread
directement et créer un travailleurQObject
, puis appelez.moveToThread ()
sur le worker utilisant le thread comme argument. Le travailleur doit contenir toutes les fonctionnalités requises pour exécuter une tâche spécifique. - Sous-classe
QThread
et réimplémentation.courir()
. L'implémentation de.courir()
doit contenir toutes les fonctionnalités requises pour exécuter une tâche spécifique.
Instancier un QThread
fournit une boucle d'événements parallèle. Une boucle d'événements permet aux objets appartenant au thread de recevoir des signaux sur leurs slots, et ces slots seront exécutés dans le thread. Sous-classement QThread
permet à l'application d'exécuter du code parallèle sans boucle d'événements.
Il y a un débat dans la communauté Qt autour de laquelle de ces approches est la meilleure pour créer des threads de travail. Cependant, la première approche est ce que la communauté et les responsables de Qt recommandent.
La première approche de création de threads de travail nécessite les étapes suivantes:
- Préparer un objet de travail en sous-classant
QObject
et mettez-y votre tâche de longue date. - Créez une nouvelle instance de la classe de travail.
- Créer un nouveau
QThread
exemple. - Déplacez l'objet worker dans le thread nouvellement créé en appelant
.moveToThread (fil)
. - Connectez les signaux et les emplacements requis pour garantir la communication inter-thread.
- Appel
.début()
sur leQThread
objet.
Vous pouvez transformer votre application d'interface graphique Freezing en une application d'interface graphique réactive en procédant comme suit:
de PyQt5.QtCore importer QObject, QThread, pyqtSignal
# Snip ...
# Étape 1: créer une classe de travail
classe Ouvrier(QObject):
fini = pyqtSignal()
le progrès = pyqtSignal(int)
def courir(soi):
"" "Tâche de longue durée." ""
pour je dans intervalle(5):
dormir(1)
soi.le progrès.émettre(je + 1)
soi.fini.émettre()
classe La fenêtre(QMainWindow):
# Snip ...
def runLongTask(soi):
# Étape 2: créer un objet QThread
soi.fil = QThread()
# Étape 3: créer un objet de travail
soi.ouvrier = Ouvrier()
# Étape 4: Déplacer le worker vers le thread
soi.ouvrier.moveToThread(soi.fil)
# Étape 5: Connectez les signaux et les slots
soi.fil.commencé.relier(soi.ouvrier.courir)
soi.ouvrier.fini.relier(soi.fil.quitter)
soi.ouvrier.fini.relier(soi.ouvrier.deleteLater)
soi.fil.fini.relier(soi.fil.deleteLater)
soi.ouvrier.le progrès.relier(soi.reportProgress)
# Étape 6: Démarrez le fil
soi.fil.début()
# Réinitialisations finales
soi.longRunningBtn.setEnabled(Faux)
soi.fil.fini.relier(
lambda: soi.longRunningBtn.setEnabled(Vrai)
)
soi.fil.fini.relier(
lambda: soi.stepLabel.Définir le texte("Étape longue: 0")
)
Tout d'abord, vous effectuez certaines importations requises. Ensuite, vous exécutez les étapes que vous avez vues auparavant.
À l'étape 1, vous créez Ouvrier
, une sous-classe de QObject
. Dans Ouvrier
, vous créez deux signaux, fini
et le progrès
. Notez que vous devez créer des signaux en tant qu'attributs de classe.
Vous créez également une méthode appelée .runLongTask ()
, où vous mettez tout le code requis pour effectuer votre tâche de longue durée. Dans cet exemple, vous simulez une tâche de longue durée à l'aide d'un pour
boucle qui itère 5
fois, avec un délai d'une seconde à chaque itération. La boucle émet également le le progrès
signal, qui indique la progression de l’opération. Finalement, .runLongTask ()
émet le fini
signal pour signaler que le traitement est terminé.
Aux étapes 2 à 4, vous créez une instance de QThread
, qui fournira l'espace pour exécuter cette tâche, ainsi qu'une instance de Ouvrier
. Vous déplacez votre objet de travail vers le thread en appelant .moveToThread ()
sur ouvrier
, en utilisant fil
comme argument.
À l'étape 5, vous connectez les signaux et emplacements suivants:
-
Le fil
commencé
signal au travailleur.runLongTask ()
slot pour vous assurer que lorsque vous démarrez le thread,.runLongTask ()
sera appelé automatiquement -
Les travailleurs
fini
signal au fil.quitter()
slot pour quitterfil
quandouvrier
termine son travail -
le
fini
signal au.deleteLater ()
insérer les deux objets pour supprimer les objets worker et thread lorsque le travail est terminé
Enfin, à l'étape 6, vous démarrez le thread en utilisant .début()
.
Une fois que le thread est en cours d'exécution, vous effectuez des réinitialisations pour que l'application se comporte de manière cohérente. Vous désactivez le Tâche de longue haleine! pour empêcher l'utilisateur de cliquer dessus pendant l'exécution de la tâche. Vous connectez également le fil fini
signal avec un lambda
fonction qui active la Tâche de longue haleine! bouton lorsque le fil est terminé. Votre connexion finale réinitialise le texte du Étape longue étiquette.
Si vous exécutez cette application, la fenêtre suivante s’affiche sur votre écran:
Depuis que vous avez déchargé la tâche de longue durée vers un thread de travail, votre application est désormais entièrement réactive. C'est ça! Vous avez utilisé avec succès PyQt QThread
pour résoudre le problème d'interface graphique figé que vous avez vu dans les sections précédentes.
Réutilisation des fils: QRunnable
et QThreadPool
Si vos applications GUI reposent fortement sur le multithreading, vous serez confronté à une surcharge importante liée à la création et à la destruction de threads. Vous devrez également tenir compte du nombre de threads que vous pouvez démarrer sur un système donné afin que vos applications restent efficaces. Heureusement, la prise en charge des threads de PyQt vous offre également une solution à ces problèmes.
Chaque application a un global pool de threads. Vous pouvez en obtenir une référence en appelant QThreadPool.globalInstance ()
.
Remarque: Même si l'utilisation du pool de threads par défaut est un choix assez courant, vous pouvez également créer votre propre pool de threads en instanciant QThreadPool
, qui fournit une collection de threads réutilisables.
Le pool de threads global maintient et gère un nombre suggéré de threads généralement basé sur le nombre de cœurs de votre processeur actuel. Il gère également la mise en file d'attente et l'exécution des tâches dans les threads de votre application. Les threads du pool sont réutilisables, ce qui évite la surcharge associée à la création et à la destruction de threads.
Pour créer des tâches et les exécuter dans un pool de threads, vous utilisez QRunnable
. Cette classe représente une tâche ou un morceau de code qui doit être exécuté. Le processus de création et d'exécution tâches exécutables comporte trois étapes:
- Sous-classe
QRunnable
et réimplémentation.courir()
avec le code de la tâche que vous souhaitez exécuter. - Instanciez la sous-classe de
QRunnable
pour créer une tâche exécutable. - Appel
QThreadPool.start ()
avec la tâche exécutable comme argument.
.courir()
doit contenir le code requis pour la tâche à accomplir. L'appel à .début()
lance votre tâche dans l'un des threads disponibles dans le pool. S'il n'y a pas de fil disponible, alors .début()
place la tâche dans la file d’attente d’exécution du pool. Lorsqu'un thread devient disponible, le code dans .courir()
est exécuté dans ce thread.
Voici une application graphique qui montre comment vous pouvez implémenter ce processus dans votre code:
1importer enregistrement
2importer Aléatoire
3importer sys
4importer temps
5
6de PyQt5.QtCore importer QRunnable, Qt, QThreadPool
septde PyQt5.QtWidgets importer (
8 QApplication,
9 QLabel,
dix QMainWindow,
11 QPushButton,
12 QVBoxLayout,
13 QWidget,
14)
15
16enregistrement.basicConfig(format="%(messages", niveau=enregistrement.INFO)
17
18# 1. Sous-classe QRunnable
19classe Runnable(QRunnable):
20 def __init__(soi, n):
21 super().__init__()
22 soi.n = n
23
24 def courir(soi):
25 # Votre tâche de longue date va ici ...
26 pour je dans intervalle(5):
27 enregistrement.Info(F"Travailler dans le fil soi.n, étape je + 1/ 5 ")
28 temps.dormir(Aléatoire.Randint(700, 2500) / 1000)
29
30classe La fenêtre(QMainWindow):
31 def __init__(soi, parent=Aucun):
32 super().__init__(parent)
33 soi.setupUi()
34
35 def setupUi(soi):
36 soi.setWindowTitle("QThreadPool + QRunnable")
37 soi.redimensionner(250, 150)
38 soi.centralWidget = QWidget()
39 soi.setCentralWidget(soi.centralWidget)
40 # Créer et connecter des widgets
41 soi.étiquette = QLabel("Bonjour le monde!")
42 soi.étiquette.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
43 countBtn = QPushButton("Cliquez sur moi!")
44 countBtn.cliqué.relier(soi.runTasks)
45 # Définir la mise en page
46 disposition = QVBoxLayout()
47 disposition.addWidget(soi.étiquette)
48 disposition.addWidget(countBtn)
49 soi.centralWidget.setLayout(disposition)
50
51 def runTasks(soi):
52 threadCount = QThreadPool.globalInstance().maxThreadCount()
53 soi.étiquette.Définir le texte(F"Fonctionnement threadCount Fils ")
54 bassin = QThreadPool.globalInstance()
55 pour je dans intervalle(threadCount):
56 # 2. Instanciez la sous-classe de QRunnable
57 exécutable = Runnable(je)
58 # 3. Début de l'appel ()
59 bassin.début(exécutable)
60
61app = QApplication(sys.argv)
62la fenêtre = La fenêtre()
63la fenêtre.spectacle()
64sys.sortie(app.exec())
Voici comment ce code fonctionne:
- Aux lignes 19 à 28, vous sous-classez
QRunnable
et réimplémentation.courir()
avec le code que vous souhaitez exécuter. Dans ce cas, vous utilisez la boucle habituelle pour simuler une tâche de longue durée. L'appel àlogging.info ()
vous informe de la progression de l’opération en imprimant un message sur l’écran de votre terminal. - En ligne 52, vous obtenez le nombre de threads disponibles. Ce nombre dépendra de votre matériel spécifique et est normalement basé sur les cœurs de votre CPU.
- En ligne 53, vous mettez à jour le texte de l'étiquette pour refléter le nombre de threads que vous pouvez exécuter.
- En ligne 55, vous démarrez un
pour
boucle qui itère sur les threads disponibles. - En ligne 57, vous instanciez
Runnable
, en passant la variable de boucleje
comme argument pour identifier le thread actuel. Alors tu appelles.début()
sur le pool de threads, en utilisant votre tâche exécutable comme argument.
Il est important de noter que certains des exemples de ce didacticiel utilisent logging.info ()
avec une configuration de base pour imprimer des messages à l'écran. Vous devez le faire car impression()
n'est pas une fonction thread-safe, donc son utilisation peut entraîner un désordre dans votre sortie. Heureusement, les fonctions de enregistrement
sont thread-safe, vous pouvez donc les utiliser dans des applications multithreads.
Si vous exécutez cette application, vous obtiendrez le comportement suivant:
Lorsque vous cliquez sur le Clique moi! bouton, l'application lance jusqu'à quatre threads. Dans le terminal d'arrière-plan, l'application signale la progression de chaque thread. Si vous fermez l'application, les threads continueront à s'exécuter jusqu'à ce qu'ils aient terminé leurs tâches respectives.
Il n'y a aucun moyen d'arrêter un QRunnable
objet de l'extérieur en Python. Pour contourner ce problème, vous pouvez créer une variable booléenne globale et la vérifier systématiquement depuis votre QRunnable
sous-classes pour les terminer lorsque votre variable devient Vrai
.
Un autre inconvénient de l'utilisation QThreadPool
et QRunnable
est-ce QRunnable
ne prend pas en charge les signaux et les slots, la communication entre threads peut donc être difficile.
D'autre part, QThreadPool
gère automatiquement un pool de threads et gère la mise en file d'attente et l'exécution des tâches exécutables dans ces threads. Les threads du pool sont réutilisables, ce qui permet de réduire la surcharge de votre application.
Communication avec les QThreads des travailleurs
Si vous effectuez une programmation multithread avec PyQt, vous devrez peut-être établir la communication entre le thread principal de votre application et vos threads de travail. Cela vous permet d'obtenir des commentaires sur la progression des threads de travail et de mettre à jour l'interface graphique en conséquence, d'envoyer des données à vos threads, d'autoriser les utilisateurs à interrompre l'exécution, etc.
Le mécanisme de signaux et d'emplacements de PyQt fournit un moyen robuste et sûr de communiquer avec les threads de travail dans une application GUI.
D'autre part, vous devrez peut-être également établir une communication entre les threads de travail, comme le partage de tampons de données ou de tout autre type de ressource. Dans ce cas, vous devez vous assurer que vous protégez correctement vos données et ressources contre les accès simultanés.
Utilisation des signaux et des slots
Un objet thread-safe est un objet auquel plusieurs threads peuvent accéder simultanément et dont l'état est garanti. Les signaux et les emplacements de PyQt sont thread-safe, vous pouvez donc les utiliser pour établir une communication entre threads ainsi que pour partager des données entre threads.
Vous pouvez connecter les signaux émis par un thread à des emplacements dans le thread ou dans un thread différent. Cela signifie que vous pouvez exécuter du code dans un thread en réponse à un signal émis dans le même thread ou dans un autre thread. Cela établit un pont de communication sûr entre les threads.
Signals can also contain data, so if you emit a signal that holds data, then you’ll receive that data in all the slots connected to the signal.
In the Responsive GUI application example, you used the signals and slots mechanism to establish communication between threads. For example, you connected the worker’s progress
signal to the application’s .reportProgress()
slot. progress
holds an integer value indicating the long-running task’s progress, and .reportProgress()
receives that value as an argument so it can update the Long-Running Step label.
Establishing connections between signals and slots in different threads is the foundation of interthread communication in PyQt. At this point, a good exercise for you to try might be to use a QToolBar
object instead of the Long-Running Step label to show the progress of the operation in the Responsive GUI application using signals and slots.
Sharing Data Between Threads
Creating multithreaded applications often requires that multiple threads have access to the same data or resources. If multiple threads access the same data or resource concurrently, and at least one of them writes or modifies this shared resource, then you might face crashes, memory or data corruption, deadlocks, or other issues.
There are at least two approaches that allow you to protect your data and resources against concurrent access:
-
Avoid shared state with the following techniques:
-
Synchronize access to a shared state with the following techniques:
If you need to share resources, then you should use the second approach. Atomic operations are carried out in a single execution step, so they can’t be interrupted by other threads. They ensure that only one thread will modify a resource at a given time.
Remarque: For a reference on how CPython manages atomic operations, check out What kinds of global value mutation are thread-safe?
Note that other Python implementations may behave differently, so if you’re using a different implementation, then take a look at its documentation for further detail on atomic operations and thread safety.
Mutual exclusion is a common pattern in multithreaded programming. Access to data and resources is protected using locks, which are a synchronization mechanism that typically allows only one thread to access a resource at a given time.
For example, if thread A needs to update a global variable, then it can acquire a lock on that variable. This prevents thread B from accessing the variable at the same time. Once thread A finishes updating the variable, it releases the lock, and thread B can access the variable. This is based on the principle mutual exclusion, which enforces synchronized access by making threads wait for one another when accessing data and resources.
It’s important to mention that using locks has a significant cost and can reduce the overall performance of your application. Thread synchronization forces most threads to wait until a resource becomes available, so you won’t be taking advantage of parallel execution anymore.
PyQt provides a few convenient classes for protecting resources and data from concurrent access:
-
QMutex
is a lock class that allows you to manage mutual exclusion. You can lock a mutex in a given thread to gain exclusive access to a shared resource. Once the mutex is unlocked, other threads can get access to the resource. -
QReadWriteLock
is similar toQMutex
but distinguishes between reading and writing access. With this type of lock, you can allow multiple threads to have simultaneous read-only access to a shared resource. If a thread needs to write to the resource, then all other threads must be blocked until the writing is complete. -
QSemaphore
is a generalization ofQMutex
that protects a certain number of identical resources. If a semaphore is protecting n resources, and you try to lock n + 1 resources, then the semaphore gets blocked, preventing threads from accessing the resources.
With PyQt’s lock classes, you can secure your data and resources and prevent a lot of problems. The next section shows an example of how to use QMutex
for these purposes.
Multithreading in PyQt: Best Practices
There are a few best practices that you can apply when building multithreaded applications in PyQt. Here’s a non-exhaustive list:
- Avoid launching long-running tasks in the main thread of a PyQt application.
- Utilisation
QObject.moveToThread()
etQThread
objects to create worker threads. - Utilisation
QThreadPool
etQRunnable
if you need to manage a pool of worker threads. - Use signals and slots to establish safe interthread communication.
- Utilisation
QMutex
,QReadWriteLock
, orQSemaphore
to prevent threads from accessing shared data and resources concurrently. - Make sure to unlock or release
QMutex
,QReadWriteLock
, orQSemaphore
before finishing a thread. - Release the lock in all possible execution paths in functions with multiple
revenir
statements. - Don’t try to create, access, or update GUI components or widgets from a worker thread.
- Don’t try to move a
QObject
with a parent-child relationship to a different thread.
If you consistently apply these best practices when working with threads in PyQt, then your applications will be less error-prone and more accurate and robust. You’ll prevent problems like data corruption, deadlocks, race conditions, and others. You’ll also provide a better experience for your users.
Conclusion
Executing long-running tasks in a PyQt application’s main thread might cause the application’s GUI to freeze and becomes unresponsive. This is a common issue in GUI programming and can result in a bad user experience. Creating worker threads with PyQt’s QThread
to offload long-running tasks effectively works around this issue in your GUI applications.
In this tutorial, you’ve learned how to:
- Use PyQt’s
QThread
to prevent GUI applications from freezing - Créer réutilisable
QThread
objects with PyQt’sQThreadPool
etQRunnable
- Use signals and slots for interthread communication in PyQt
- Utilisation shared resources safely with PyQt’s lock classes
You also learned some best practices that apply to multithreaded programming with PyQt and its built-in thread support.
[ad_2]