Le threading permet d'exécuter en parallèle différentes parties d'un même processus (Source : RealPython). Ces différentes parties sont généralement indépendantes et correspondent à des unités d'exécution distinctes appartenant au même processus. Un processus n'est rien d'autre qu'un programme en cours d'exécution, composé d'unités pouvant s'exécuter concurremment. Par exemple : un navigateur web est un processus, une application qui pilote plusieurs caméras en même temps en est un autre ; un jeu vidéo en est également un.
\n
Au sein d'un processus, on parle de multi-threading lorsque plusieurs threads collaborent pour atteindre un même objectif. L'intérêt principal des threads est de permettre une exécution parallèle du programme.
\n
Illustrons le concept avec un exemple. Imaginez une application qui compte les voitures entrant et sortant du parking d'un centre commercial. Votre dispositif relie plusieurs caméras à un boîtier central pour surveiller entrées et sorties. Chaque caméra exécute un algorithme de comptage au sein du même processus. Cependant, chaque caméra et l'algorithme qui la traite peuvent tourner dans un thread distinct. De même, la lecture des images de la caméra et la prédiction de l'algorithme sur ces images peuvent être gérées par deux threads séparés.
\n
Autre exemple : dans un jeu vidéo, le processus doit traiter en parallèle les graphismes, les interactions utilisateur et le réseau (en mode multijoueur) pour rester réactif en permanence. Pour cela, il s'appuie sur le multi-threading, chaque thread prenant en charge une tâche indépendante.
\n
Un thread possède son propre flot d'exécution, ce qui signifie que plusieurs opérations se déroulent simultanément au sein du processus.
\n
Important : chaque processus dispose au minimum d'un thread, appelé main thread. Si votre programme ne crée aucun thread, il fonctionne au moins sur ce thread principal, c‑est‑à‑dire le processus lui-même.
\n
En observant le schéma ci‑dessus, on voit un processus composé du thread principal contenant des variables globales, et de trois threads différents t1, t2 et t3. Chaque thread a ses variables locales et son propre flux d'exécution. Les threads partagent généralement les ressources du processus : les ressources accessibles au thread principal sont également partagées avec t1, t2 et t3. Tous les threads accèdent aux variables globales tout en conservant leurs variables locales.
\n
Il existe différents types de threads :
\n
\n
Thread noyau (kernel thread)
\n
Thread utilisateur
\n
Combinaison de thread noyau et utilisateur
\n
\n
Voyons maintenant quelques avantages à utiliser des threads dans votre programme :
\n
\n
\n
Le multi-threading peut accélérer l'exécution si plusieurs cœurs CPU sont disponibles.
\n
\n
\n
Il permet aussi d'effectuer d'autres tâches pendant les opérations d'E/S grâce à plusieurs threads, voire au thread principal associé à un thread unique. Par exemple, la vitesse de lecture des images par la caméra et celle de l'inférence de l'algorithme peuvent être gérées par des threads différents : l'algorithme n'attend pas la prochaine image, et la lecture des images n'attend pas la fin de l'inférence pour continuer.
\n
\n
\n
\n
Les threads d'un même processus peuvent partager la mémoire et les ressources du thread principal.
\n
\n
Les défis du threading
\n
\n
\n
N'oubliez pas que Python s'appuie sur l'implémentation CPython, qui limite l'exécution à un seul thread à la fois ; le threading ne va donc pas accélérer toutes les tâches. La raison principale est le Global Interpreter Lock (GIL).
\n
Pour en savoir plus sur le GIL, consultez ce tutoriel.
\n
\n
\n
Si vous cherchez à accélérer une tâche très consommatrice en CPU, le threading n'est sans doute pas la meilleure option. Dans ce cas, le multi-processing est souvent plus adapté.
\n
\n
\n
Pour exécuter un programme composé de nombreux threads, il faut pouvoir basculer rapidement de l'un à l'autre ; la planification (scheduling) doit donc être soignée dans les programmes très multi-threadés.
\n
\n
\n
Le partage des ressources peut poser problème : tous les threads accèdent aux mêmes ressources et à la même mémoire globale. Une opération dans un thread peut provoquer une erreur mémoire dans un autre, ou l'empêcher d'obtenir la mémoire nécessaire à son exécution.
\n
\n
\n
Le threading en Python
\n
\n
\n
En Python, le module intégré pour le threading s'appelle threading et peut être importé directement.
\n
\n
\n
Comme presque tout en Python est un objet, un thread est lui aussi un objet. Un thread peut :
\n
\n
Conserver des données,
\n
Être stocké dans des structures comme des dictionnaires, listes, ensembles, etc.
\n
Être passé en paramètre à une fonction.
\n
\n
\n
\n
Un thread peut aussi être exécuté comme un processus.
\n
\n
\n
Un thread en Python peut adopter différents états :
\n
\n
Attente (wait),
\n
Verrouillé (locked).
\n
\n
\n
\n
Voyons maintenant comment implémenter le threading en Python.
\n
Module thread en Python 3
\n
Notez que Python 3 reste compatible avec le module thread présent en Python 2.7. En Python 3, il s'importe sous le nom de module _thread. Prenons un exemple pour comprendre _thread.
\n
import _thread #thread module imported\nimport time #time module\nPropulsé par
\n
Définissons une fonction thread_delay qui prend deux paramètres en entrée : le nom du thread et le délai. Dans cette fonction, vous allez :
\n
\n
Initialiser un compteur à zéro,
\n
Boucler avec une boucle while exécutée trois fois,
\n
Dans la boucle, utiliser time.sleep() pour ajouter un délai, utile pour visualiser l'exécution du thread (en secondes),
\n
Incrémenter le compteur de 1,
\n
Afficher le nom du thread et l'horodatage pour suivre son exécution.
\n
\n
defthread_delay(thread_name, delay):\n count =0\n while count <3:\n time.sleep(delay)\n count +=1\n print(thread_name,'-------->', time.time())\nPropulsé par
\n
Pour exécuter la fonction ci‑dessus dans un thread, utilisez la méthode start_new_thread du module _thread.
\n
Regardons la docstring de la méthode start_new_thread.
Passons la fonction thread_delay sans parenthèses ainsi que deux arguments : le nom du thread et le délai (pour visualiser l'exécution, très rapide sinon).
\n
_thread.start_new_thread(thread_delay,('t1',1))\n_thread.start_new_thread(thread_delay,('t2',3))\nPropulsé par
\n
10916\n\n\n\nt1 -------->1588077856.4221454\nt1 -------->1588077857.4418247\nt2 -------->1588077858.422453\nt1 -------->1588077858.4431\nt2 -------->1588077861.4371448\nt2 -------->1588077864.438915\nPropulsé par
\n
On constate que le thread t1 commence à s'exécuter en premier. Pendant ce temps, t2 attend en raison d'un délai de 3 secondes ; dès que ce délai se termine, t2 s'exécute, alors que le délai de t1 n'est que d'une seconde.
\n
Modifions maintenant le délai de t2 à 5 secondes, ce qui fera démarrer t2 après la fin de t1, puisque t1 met seulement 3 secondes à s'exécuter.
\n
_thread.start_new_thread(thread_delay,('t1',1))\n_thread.start_new_thread(thread_delay,('t2',5))\nPropulsé par
\n
14632\n\n\n\nt1 -------->1588078001.4087512\nt1 -------->1588078002.4272866\nt1 -------->1588078003.432218\nt2 -------->1588078005.4082909\nt2 -------->1588078010.4268317\nt2 -------->1588078015.4283602\nPropulsé par
\n
Implémenter le threading avec le module Threading
\n
Reprenons le même exemple, mais en utilisant cette fois le module threading au lieu de _thread.
t1 -------->1588081121.7790291\nt1 -------->1588081122.7962046\nt2 -------->1588081123.7789369\nt1 -------->1588081123.7980876\nt2 -------->1588081126.7961557\nt2 -------->1588081129.7995002\nPropulsé par
\n
Vous utiliserez aussi la méthode join, qui attend la fin de tous les threads. Ainsi, le code situé après les join ne s'exécutera qu'une fois les threads terminés.
t1.start()\nt2.start()\n\nt1.join()\nt2.join()\n\nprint(\"Thread execution is complete!\")\nPropulsé par
\n
t1 -------->1588081798.7118263\nt1 -------->1588081799.7231226\nt2 -------->1588081800.7129223\nt1 -------->1588081800.730732\nt2 -------->1588081803.7298765\nt2 -------->1588081806.7309563\nThread execution is complete!\nPropulsé par
\n
Calculons maintenant le volume d'un cube et d'un carré en utilisant le threading : c'est très simple.
\n
defvolume_cube(a):\n print(\"Volume of Cube:\", a*a*a)\nPropulsé par
\n
defvolume_square(a):\n print(\"Volume of Square:\", a*a)\nPropulsé par
\n
t1 = threading.Thread(target=volume_cube, args=(2))\nt2 = threading.Thread(target=volume_square, args=(3))\nPropulsé par
\n
t1.start()\nt2.start()\n\nt1.join()\nt2.join()\n\nprint(\"Thread execution is complete!\")\nPropulsé par
\n
Thread execution is complete!\n\n\nException in thread Thread-9:\nTraceback(most recent call last):\n File \"C:\\Program Files\\Anaconda3\\lib\\threading.py\", line 926,in _bootstrap_inner\n self.run()\n File \"C:\\Program Files\\Anaconda3\\lib\\threading.py\", line 870,in run\n self._target(*self._args,**self._kwargs)\nTypeError:volume_square() argument after * must be an iterable, not int\nException in thread Thread-8:\nTraceback(most recent call last):\n File \"C:\\Program Files\\Anaconda3\\lib\\threading.py\", line 926,in _bootstrap_inner\n self.run()\n File \"C:\\Program Files\\Anaconda3\\lib\\threading.py\", line 870,in run\n self._target(*self._args,**self._kwargs)\nTypeError:volume_cube() argument after * must be an iterable, not int\nPropulsé par
\n
Comment ça ? Dès l'appel à start, une erreur survient. C'est parce que le paramètre args attend un tuple. Or volume_cube et volume_square n'ont qu'un seul paramètre : il faut donc ajouter une virgule dans args après la première valeur.
\n
t1 = threading.Thread(target=volume_cube, args=(2,))\nt2 = threading.Thread(target=volume_square, args=(3,))\nPropulsé par
\n
t1.start()\nt2.start()\n\nt1.join()\nt2.join()\n\nprint(\"Thread execution is complete!\")\nPropulsé par
\n
Volume ofCube:8\nVolume ofSquare:9\nThread execution is complete!\nPropulsé par
\n
Voyons maintenant comment utiliser le threading via une sous-classe.
\n
Threading comme sous-classe
\n
Dans cette section, vous allez créer une sous-classe de la classe Thread fournie par le module threading, puis instancier des threads t1 et t2 à partir de celle‑ci.
Starting Thread: t1\nStarting Thread: t2\nt1 -------->1588089083.4879806\nt1 -------->1588089084.489222\nt1 -------->1588089085.4909666\nExecution ofThread: t1 is complete!\nt2 -------->1588089085.4979634\nt2 -------->1588089088.499365\nt2 -------->1588089091.5012596\nExecution ofThread: t2 is complete!\nThread execution is complete!\nPropulsé par
\n
Explications pas à pas :
\n
\n
\n
Vous définissez une classe DataCampThread dont l'argument de classe parent est threading.Thread. L'objectif est de créer une sous‑classe du module threading. Vous bénéficiez ainsi de toutes les méthodes de threading.Thread, comme avec l'héritage.
\n
\n
\n
Vous définissez ensuite la méthode __init__ de DataCampThread et y passez le nom et le délai. C'est le constructeur, analogue aux paramètres transmis à threading.Thread, mais ici à la classe DataCampThread.
\n
\n
\n
Vous initialisez le __init__ de la classe mère threading.Thread, puis affectez name et delay.
\n
\n
\n
Vous définissez la méthode run. Rappelez‑vous ici l'appel à start : sous le capot, il invoque run. Puisque vous héritez de la classe mère, vous pouvez surcharger run. Ainsi, lors de l'appel à start, vos modifications dans run seront prises en compte.
\n
\n
\n
Conclusion
\n
Félicitations pour avoir terminé ce tutoriel.
\n
Ce tutoriel proposait une introduction aux bases du threading en Python. Le sujet est vaste et essentiel ; certains aspects n'ont volontairement pas été abordés. N'hésitez pas à explorer d'autres méthodes comme active_count, current_thread, etc.
Tutoriel complet d'introduction aux boucles Python. Apprenez et pratiquez les boucles while et for, les boucles imbriquées, les mots-clés break et continue, la fonction range et bien plus encore.
Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Découvrez les fonctions lambda Python, leur utilité et quand les utiliser. Comprend des exemples pratiques et des bonnes pratiques pour une mise en œuvre efficace.
Découvrez les fonctions et méthodes des listes Python. Veuillez suivre les exemples de code pour list() et d'autres fonctions et méthodes Python dès maintenant.
Si vous débutez en Python et souhaitez aller plus loin, suivez le cours Introduction to Data Science in Python de DataCamp.
\nIntroduction
\nLe threading permet d'exécuter en parallèle différentes parties d'un même processus (Source : RealPython). Ces différentes parties sont généralement indépendantes et correspondent à des unités d'exécution distinctes appartenant au même processus. Un processus n'est rien d'autre qu'un programme en cours d'exécution, composé d'unités pouvant s'exécuter concurremment. Par exemple : un navigateur web est un processus, une application qui pilote plusieurs caméras en même temps en est un autre ; un jeu vidéo en est également un.
\nAu sein d'un processus, on parle de multi-threading lorsque plusieurs threads collaborent pour atteindre un même objectif. L'intérêt principal des threads est de permettre une exécution parallèle du programme.
\nIllustrons le concept avec un exemple. Imaginez une application qui compte les voitures entrant et sortant du parking d'un centre commercial. Votre dispositif relie plusieurs caméras à un boîtier central pour surveiller entrées et sorties. Chaque caméra exécute un algorithme de comptage au sein du même processus. Cependant, chaque caméra et l'algorithme qui la traite peuvent tourner dans un thread distinct. De même, la lecture des images de la caméra et la prédiction de l'algorithme sur ces images peuvent être gérées par deux threads séparés.
\nAutre exemple : dans un jeu vidéo, le processus doit traiter en parallèle les graphismes, les interactions utilisateur et le réseau (en mode multijoueur) pour rester réactif en permanence. Pour cela, il s'appuie sur le multi-threading, chaque thread prenant en charge une tâche indépendante.
\nUn thread possède son propre flot d'exécution, ce qui signifie que plusieurs opérations se déroulent simultanément au sein du processus.
\nImportant : chaque processus dispose au minimum d'un thread, appelé
\nmain thread. Si votre programme ne crée aucun thread, il fonctionne au moins sur ce thread principal, c‑est‑à‑dire le processus lui-même.En observant le schéma ci‑dessus, on voit un processus composé du thread principal contenant des variables globales, et de trois threads différents t1, t2 et t3. Chaque thread a ses variables locales et son propre flux d'exécution. Les threads partagent généralement les ressources du processus : les ressources accessibles au thread principal sont également partagées avec t1, t2 et t3. Tous les threads accèdent aux variables globales tout en conservant leurs variables locales.
\nIl existe différents types de threads :
\n\n- Thread noyau (kernel thread)
\n- Thread utilisateur
\n- Combinaison de thread noyau et utilisateur
\n
\nVoyons maintenant quelques avantages à utiliser des threads dans votre programme :
\n\n- \n
\n- \n
\n
\nLe multi-threading peut accélérer l'exécution si plusieurs cœurs CPU sont disponibles.
\nIl permet aussi d'effectuer d'autres tâches pendant les opérations d'E/S grâce à plusieurs threads, voire au thread principal associé à un thread unique. Par exemple, la vitesse de lecture des images par la caméra et celle de l'inférence de l'algorithme peuvent être gérées par des threads différents : l'algorithme n'attend pas la prochaine image, et la lecture des images n'attend pas la fin de l'inférence pour continuer.
\n\n- Les threads d'un même processus peuvent partager la mémoire et les ressources du thread principal.
\n
\nLes défis du threading
\n\n- \n
\n- \n
\n- \n
\n- \n
\n
\nN'oubliez pas que Python s'appuie sur l'implémentation CPython, qui limite l'exécution à un seul thread à la fois ; le threading ne va donc pas accélérer toutes les tâches. La raison principale est le Global Interpreter Lock (GIL).
\nPour en savoir plus sur le GIL, consultez ce tutoriel.
\nSi vous cherchez à accélérer une tâche très consommatrice en CPU, le threading n'est sans doute pas la meilleure option. Dans ce cas, le multi-processing est souvent plus adapté.
\nPour exécuter un programme composé de nombreux threads, il faut pouvoir basculer rapidement de l'un à l'autre ; la planification (scheduling) doit donc être soignée dans les programmes très multi-threadés.
\nLe partage des ressources peut poser problème : tous les threads accèdent aux mêmes ressources et à la même mémoire globale. Une opération dans un thread peut provoquer une erreur mémoire dans un autre, ou l'empêcher d'obtenir la mémoire nécessaire à son exécution.
\nLe threading en Python
\n\n- \n
\n- \n
\n- \n
\n- \n
\n
\nEn Python, le module intégré pour le threading s'appelle
\nthreadinget peut être importé directement.Comme presque tout en Python est un objet, un thread est lui aussi un objet. Un thread peut :
\n\n- Conserver des données,
\n- Être stocké dans des structures comme des dictionnaires, listes, ensembles, etc.
\n- Être passé en paramètre à une fonction.
\n
\nUn thread peut aussi être exécuté comme un processus.
\nUn thread en Python peut adopter différents états :
\n\n- Attente (wait),
\n- Verrouillé (locked).
\n
\nVoyons maintenant comment implémenter le threading en Python.
\nModule thread en Python 3
\nNotez que Python 3 reste compatible avec le module
\n\nthreadprésent en Python 2.7. En Python 3, il s'importe sous le nom de module_thread. Prenons un exemple pour comprendre_thread.Définissons une fonction
\nthread_delayqui prend deux paramètres en entrée : le nom du thread et le délai. Dans cette fonction, vous allez :\n- Initialiser un compteur à zéro,
\n- Boucler avec une boucle while exécutée trois fois,
\n- Dans la boucle, utiliser
\n- Incrémenter le compteur de 1,
\n- Afficher le nom du thread et l'horodatage pour suivre son exécution.
\n
\n\ntime.sleep()pour ajouter un délai, utile pour visualiser l'exécution du thread (en secondes),Pour exécuter la fonction ci‑dessus dans un thread, utilisez la méthode
\nstart_new_threaddu module_thread.Regardons la docstring de la méthode
\n\nstart_new_thread.Passons la fonction
\n\n\nthread_delaysans parenthèses ainsi que deux arguments : le nom du thread et le délai (pour visualiser l'exécution, très rapide sinon).On constate que le thread
\nt1commence à s'exécuter en premier. Pendant ce temps,t2attend en raison d'un délai de 3 secondes ; dès que ce délai se termine,t2s'exécute, alors que le délai det1n'est que d'une seconde.Modifions maintenant le délai de
\n\n\nt2à 5 secondes, ce qui fera démarrert2après la fin det1, puisquet1met seulement 3 secondes à s'exécuter.Implémenter le threading avec le module Threading
\nReprenons le même exemple, mais en utilisant cette fois le module
\n\n\nthreadingau lieu de_thread.Le module
\nthreadingpropose une classeThread, dans le même esprit que la fonctionstart_new_threaddu module_thread.Affichons la docstring de la classe
\n\nThread, qui accepte plusieurs paramètres comme group, target (une fonction), args, etc.Dans le constructeur de Thread, passez la fonction cible
\n\nthread_delayet ses arguments.Avec le module threading, pour lancer un thread, utilisez la méthode
\n\nstart(), chargée de démarrer son exécution.Vous utiliserez aussi la méthode
\n\njoin, qui attend la fin de tous les threads. Ainsi, le code situé après lesjoinne s'exécutera qu'une fois les threads terminés.Calculons maintenant le volume d'un cube et d'un carré en utilisant le threading : c'est très simple.
\n\n\n\n\n\nComment ça ? Dès l'appel à
\n\n\n\nstart, une erreur survient. C'est parce que le paramètreargsattend un tuple. Orvolume_cubeetvolume_squaren'ont qu'un seul paramètre : il faut donc ajouter une virgule dansargsaprès la première valeur.Voyons maintenant comment utiliser le threading via une sous-classe.
\nThreading comme sous-classe
\nDans cette section, vous allez créer une sous-classe de la classe Thread fournie par le module threading, puis instancier des threads
\nt1ett2à partir de celle‑ci.Nous allons réutiliser la fonction
\n\n\nthread_delay.Explications pas à pas :
\n\n- \n
\n- \n
\n- \n
\n- \n
\n
\nVous définissez une classe
\nDataCampThreaddont l'argument de classe parent estthreading.Thread. L'objectif est de créer une sous‑classe du module threading. Vous bénéficiez ainsi de toutes les méthodes dethreading.Thread, comme avec l'héritage.Vous définissez ensuite la méthode
\n__init__deDataCampThreadet y passez le nom et le délai. C'est le constructeur, analogue aux paramètres transmis àthreading.Thread, mais ici à la classeDataCampThread.Vous initialisez le
\n__init__de la classe mèrethreading.Thread, puis affecteznameetdelay.Vous définissez la méthode
\nrun. Rappelez‑vous ici l'appel àstart: sous le capot, il invoquerun. Puisque vous héritez de la classe mère, vous pouvez surchargerrun. Ainsi, lors de l'appel àstart, vos modifications dansrunseront prises en compte.Conclusion
\nFélicitations pour avoir terminé ce tutoriel.
\nCe tutoriel proposait une introduction aux bases du threading en Python. Le sujet est vaste et essentiel ; certains aspects n'ont volontairement pas été abordés. N'hésitez pas à explorer d'autres méthodes comme active_count, current_thread, etc.
\nPour aller plus loin, consultez aussi la documentation Python sur le parallélisme à base de threads.
\nSi vous débutez en Python et souhaitez approfondir, suivez le cours Introduction to Data Science in Python de DataCamp.