Utilisez QThread de PyQt pour empêcher le gel des interfaces graphiques – Real Python

By | décembre 21, 2020

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 et QRunnable
  • 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:

Exemple d'interface graphique PyQt Freezing

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.

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:

Exemple d'interface graphique PyQt Freezing

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.

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:

  1. L'unité centrale de traitement (CPU)
  2. 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:

  1. Rendre vos applications plus rapides en tirant parti des processeurs multicœurs
  2. Simplifier la structure de l'application en la divisant en sous-tâches plus petites
  3. 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:

  1. Fil principal
  2. 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:

  1. Instancier QThread directement et créer un travailleur QObject, 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.
  2. 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:

  1. Préparer un objet de travail en sous-classant QObject et mettez-y votre tâche de longue date.
  2. Créez une nouvelle instance de la classe de travail.
  3. Créer un nouveau QThread exemple.
  4. Déplacez l'objet worker dans le thread nouvellement créé en appelant .moveToThread (fil).
  5. Connectez les signaux et les emplacements requis pour garantir la communication inter-thread.
  6. Appel .début() sur le QThread 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 quitter fil quand ouvrier 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:

Exemple d'interface graphique réactive PyQt

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 ().

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:

  1. Sous-classe QRunnable et réimplémentation .courir() avec le code de la tâche que vous souhaitez exécuter.
  2. Instanciez la sous-classe de QRunnable pour créer une tâche exécutable.
  3. 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 boucle je 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:

Exemple PyQt QRunnable

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:

  1. Avoid shared state with the following techniques:

  2. 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.

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 to QMutex 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 of QMutex 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.

Protecting Shared Data With QMutex

QMutex is commonly used in multithreaded PyQt applications to prevent multiple threads from accessing shared data and resources concurrently. In this section, you’ll code a GUI application that uses a QMutex object to protect a global variable from concurrent write access.

To learn how to use QMutex, you’ll code an example that manages a bank account from which two people can withdraw money at any time. In this case, you need to protect the account balance from parallel access. Otherwise, people could end up withdrawing more money than they have in the bank.

For example, suppose you have an account with $100. Two people check the available balance at the same time and see that the account has $100. They each think that they can withdraw $60 and leave $40 in the account, so they proceed with the transaction. The resulting balance in the account will be -$20, which might be a significant problem.

To code the example, you’ll start by importing the required modules, functions, and classes. You also add a basic logging configuration and define two global variables:

import logging
import random
import sys
de temps import dormir

de PyQt5.QtCore import QMutex, QObject, QThread, pyqtSignal
de PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

logging.basicConfig(format="%(message)s", level=logging.INFO)

balance = 100.00
mutex = QMutex()

balance is a global variable that you’ll use to store the current balance in the bank account. mutex est un QMutex object that you’ll use to protect balance from parallel access. In other words, with mutex, you’ll prevent multiple threads from accessing balance at the same time.

The next step is to create a subclass of QObject that holds the code for managing how to withdraw money from the bank account. You’ll call that class AccountManager:

class AccountManager(QObject):
    finished = pyqtSignal()
    updatedBalance = pyqtSignal()

    def withdraw(self, person, amount):
        logging.info("%s    wants to withdraw $%.2f...", person, amount)
        global balance
        mutex.lock()
        si balance - amount >= 0:
            dormir(1)
            balance -= amount
            logging.info("-$%.2f    accepted", amount)
        autre:
            logging.info("-$%.2f    rejected", amount)
        logging.info("===Balance===: $%.2f", balance)
        self.updatedBalance.emit()
        mutex.unlock()
        self.finished.emit()

Dans AccountManager, you first define two signals:

  1. finished indicates when the class processes its work.
  2. updatedBalance indicates when balance gets updated.

Then you define .withdraw(). In this method, you do the following:

  • Show a message that points out the person who wants to withdraw some money
  • Use a global statement to use balance from within .withdraw()
  • Call .lock() sur mutex to acquire the lock and protect the balance from parallel access
  • Check if the account balance allows withdrawing the amount at hand
  • Call sleep() to simulate that the operation takes some time to complete
  • Decrement the balance by the required amount of money
  • Show messages to notify if the transaction was accepted or not
  • Emit the updatedBalance signal to notify that the balance has been updated
  • Release the lock to allow other threads to access balance
  • Emit the finished signal to notify that the operation has finished

This application will show a window like this:

Account Manager GUI

Here’s the required code for creating this GUI:

class Window(QMainWindow):
    def __init__(self, parent=Aucun):
        super().__init__(parent)
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Account Manager")
        self.resize(200, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        bouton = QPushButton("Withdraw Money!")
        bouton.clicked.connect(self.startThreads)
        self.balanceLabel = QLabel(f"Current Balance: $balance:,.2f")
        layout = QVBoxLayout()
        layout.addWidget(self.balanceLabel)
        layout.addWidget(bouton)
        self.centralWidget.setLayout(layout)

le Current Balance label shows the account’s available balance. If you click the Withdraw Money! button, then the application will simulate two people trying to withdraw money from the account at the same time. You’ll simulate these two people using threads:

class Window(QMainWindow):
    # Snip...
    def createThread(self, person, amount):
        thread = QThread()
        worker = AccountManager()
        worker.moveToThread(thread)
        thread.started.connect(lambda: worker.withdraw(person, amount))
        worker.updatedBalance.connect(self.updateBalance)
        worker.finished.connect(thread.quit)
        worker.finished.connect(worker.deleteLater)
        thread.finished.connect(thread.deleteLater)
        revenir thread

This method contains the required code for creating a thread for each person. In this example, you connect the thread’s started signal with the worker’s .withdraw(), so when the thread starts, this method will run automatically. You also connect the worker’s updatedBalance signal to a method called .updateBalance(). This method will update the Current Balance label with the current account balance.

Here’s the code for .updateBalance():

class Window(QMainWindow):
    # Snip...
    def updateBalance(self):
        self.balanceLabel.setText(f"Current Balance: $balance:,.2f")

Anytime a person withdraws money, the account’s balance gets reduced by the requested amount. This method updates the text of the Current Balance label to reflect the changes in the account balance.

To complete the application, you need to create the two people and start a thread for each of them:

class Window(QMainWindow):class Window(QMainWindow):
    def __init__(self, parent=Aucun):
        super().__init__(parent)
        self.setupUi()
        self.threads = []

    # Snip...
    def startThreads(self):
        self.threads.clear()
        personnes = 
            "Alice": random.randint(100, 10000) / 100,
            "Bob": random.randint(100, 10000) / 100,
        
        self.threads = [[[[
            self.createThread(person, amount)
            for person, amount dans personnes.items()
        ]
        for thread dans self.threads:
            thread.début()

First, you add .threads as an instance attribute to the initializer of your Window. This variable will hold a list of threads to prevent the threads from getting out of scope once .startThreads() returns. Then you define .startThreads() to create two people and a thread for each of them.

Dans .startThreads(), you perform the following operations:

  • Clear the thread in .threads if any to remove threads that have been destroyed already
  • Create a dictionary containing two people, Alice et Bob. Each person will try to withdraw a random amount of money from the bank account
  • Create a thread for each person using a list comprehension and .createThread()
  • Start the threads in a for loop

With this last piece of code, you’re almost done. You just need to create the application and the window and then run the event loop:

app = QApplication(sys.argv)
window = Window()
window.spectacle()
sys.exit(app.exec())

If you run this application from your command line, then you’ll get the following behavior:

Account Manager Example

The output in the background terminal shows that the threads work. Using a QMutex object in this example allows you to protect the bank account balance and synchronize the access to it. This prevents users from withdrawing an amount of money that exceeds the available balance.

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() et QThread objects to create worker threads.
  • Utilisation QThreadPool et QRunnable if you need to manage a pool of worker threads.
  • Use signals and slots to establish safe interthread communication.
  • Utilisation QMutex, QReadWriteLock, or QSemaphore to prevent threads from accessing shared data and resources concurrently.
  • Make sure to unlock or release QMutex, QReadWriteLock, or QSemaphore 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’s QThreadPool et QRunnable
  • 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.