0% ont trouvé ce document utile (0 vote)
30 vues98 pages

Tutoriel Threading en Python

Livre qui vous montre comment créer des programmes grâce aux threads en python

Transféré par

daryl emani
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
30 vues98 pages

Tutoriel Threading en Python

Livre qui vous montre comment créer des programmes grâce aux threads en python

Transféré par

daryl emani
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd

Tutoriel sur la concurrence dans Python

La concurrence, phénomène naturel, est la survenance de deux ou plusieurs événements en


même temps. C'est une tâche difficile pour les professionnels de créer des applications
simultanées et de tirer le meilleur parti du matériel informatique.
Ce didacticiel sera utile pour les diplômés, les étudiants de troisième cycle et les étudiants en
recherche qui s'intéressent à ce sujet ou qui ont ce sujet dans leur programme. Le lecteur peut
être un débutant ou un apprenant avancé.
Le lecteur doit avoir des connaissances de base sur des concepts tels que la concurrence, le
multitraitement, les threads et les processus, etc. du système d'exploitation. Il / elle doit
également être conscient des terminologies de base utilisées dans le système d'exploitation ainsi
que des concepts de programmation Python.

Concurrence en Python - Introduction


Dans ce chapitre, nous allons comprendre le concept de concurrence en Python et découvrir les
différents threads et processus.

Qu'est-ce que la concurrence?


En termes simples, la concurrence est l'occurrence de deux événements ou plus en même
temps. La concurrence est un phénomène naturel car de nombreux événements se produisent
simultanément à un moment donné.
En termes de programmation, la concurrence se produit lorsque deux tâches se chevauchent en
exécution. Grâce à la programmation simultanée, les performances de nos applications et de nos
systèmes logiciels peuvent être améliorées car nous pouvons traiter simultanément les
demandes plutôt que d'attendre qu'une précédente soit terminée.

Revue historique de la concurrence


Les points suivants nous donneront un bref aperçu historique de la concurrence -

Du concept des chemins de fer


La concurrence est étroitement liée au concept des chemins de fer. Avec les chemins de fer, il
était nécessaire de gérer plusieurs trains sur le même système ferroviaire de manière à ce que
chaque train arrive à destination en toute sécurité.

Calcul simultané dans le milieu universitaire


L'intérêt pour la concurrence informatique a commencé avec le document de recherche publié
par Edsger W. Dijkstra en 1965. Dans cet article, il a identifié et résolu le problème de l'exclusion
mutuelle, la propriété du contrôle de la concurrence.

Primitives de concurrence de haut niveau


Ces derniers temps, les programmeurs obtiennent des solutions simultanées améliorées en
raison de l'introduction de primitives de concurrence de haut niveau.
Amélioration de la concurrence avec les langages de
programmation
Les langages de programmation tels que Golang, Rust et Python de Google ont réalisé des
développements incroyables dans des domaines qui nous aident à obtenir de meilleures
solutions simultanées.

Qu'est-ce que le thread et le


multithreading?
Threadest la plus petite unité d'exécution pouvant être effectuée dans un système d'exploitation.
Ce n'est pas en soi un programme mais s'exécute dans un programme. En d'autres termes, les
threads ne sont pas indépendants les uns des autres. Chaque thread partage une section de
code, une section de données, etc. avec d'autres threads. Ils sont également connus sous le nom
de processus légers.
Un thread se compose des composants suivants -
 Compteur de programme composé de l'adresse de la prochaine instruction exécutable
 Stack
 Ensemble de registres
 Un identifiant unique
Multithreading, d'autre part, est la capacité d'un processeur à gérer l'utilisation du système
d'exploitation en exécutant plusieurs threads simultanément. L'idée principale du multithreading
est de réaliser le parallélisme en divisant un processus en plusieurs threads. Le concept de
multithreading peut être compris à l'aide de l'exemple suivant.

Exemple
Supposons que nous exécutions un processus particulier dans lequel nous ouvrons MS Word
pour y saisir du contenu. Un fil sera assigné pour ouvrir MS Word et un autre fil sera nécessaire
pour y taper du contenu. Et maintenant, si nous voulons modifier l'existant, un autre thread sera
nécessaire pour effectuer la tâche d'édition et ainsi de suite.

Qu'est-ce que le processus et le


multitraitement?
UNEprocessest défini comme une entité, qui représente l'unité de travail de base à mettre en
œuvre dans le système. Pour le dire simplement, nous écrivons nos programmes informatiques
dans un fichier texte et lorsque nous exécutons ce programme, il devient un processus qui
effectue toutes les tâches mentionnées dans le programme. Au cours du cycle de vie du
processus, il passe par différentes étapes: démarrage, prêt, exécution, attente et arrêt.
Le diagramme suivant montre les différentes étapes d'un processus -
Un processus ne peut avoir qu'un seul thread, appelé thread principal, ou plusieurs threads ayant
leur propre ensemble de registres, compteur de programmes et pile. Le diagramme suivant nous
montrera la différence -

Multiprocessing,d'autre part, est l'utilisation de deux ou plusieurs unités CPU dans un seul
système informatique. Notre objectif principal est d'exploiter pleinement le potentiel de notre
matériel. Pour y parvenir, nous devons utiliser le nombre total de cœurs de processeur
disponibles dans notre système informatique. Le multitraitement est la meilleure approche pour
ce faire.
Python est l'un des langages de programmation les plus populaires. Voici quelques raisons qui le
rendent adapté aux applications simultanées -

Sucre syntaxique
Le sucre syntaxique est une syntaxe dans un langage de programmation conçu pour rendre les
choses plus faciles à lire ou à exprimer. Cela rend le langage «plus doux» pour l'usage humain:
les choses peuvent être exprimées plus clairement, plus concis ou dans un style alternatif basé
sur les préférences. Python est livré avec des méthodes Magic, qui peuvent être définies pour
agir sur des objets. Ces méthodes Magic sont utilisées comme sucre syntaxique et liées à des
mots-clés plus faciles à comprendre.

Grande communauté
Le langage Python a connu un taux d'adoption massif parmi les scientifiques des données et les
mathématiciens, travaillant dans le domaine de l'IA, de l'apprentissage automatique, de
l'apprentissage en profondeur et de l'analyse quantitative.

API utiles pour la programmation simultanée


Python 2 et 3 ont un grand nombre d'API dédiées à la programmation parallèle / simultanée. Les
plus populaires d'entre eux sontthreading, concurrent.features, multiprocessing, asyncio,
gevent and greenlets, etc.

Limitations de Python dans


l'implémentation d'applications
simultanées
Python est livré avec une limitation pour les applications simultanées. Cette limitation
s'appelleGIL (Global Interpreter Lock)est présent dans Python. GIL ne nous permet jamais
d'utiliser plusieurs cœurs de CPU et nous pouvons donc dire qu'il n'y a pas de vrais threads en
Python. Nous pouvons comprendre le concept de GIL comme suit -

GIL (Global Interpreter Lock)


C'est l'un des sujets les plus controversés du monde Python. En CPython, GIL est le mutex - le
verrou d'exclusion mutuelle, qui sécurise les threads. En d'autres termes, nous pouvons dire que
GIL empêche plusieurs threads d'exécuter du code Python en parallèle. Le verrou ne peut être
détenu que par un seul thread à la fois et si nous voulons exécuter un thread, il doit d'abord
acquérir le verrou. Le diagramme ci-dessous vous aidera à comprendre le fonctionnement de
GIL.

Cependant, il existe certaines bibliothèques et implémentations en Python telles que Numpy,


Jpython et IronPytbhon. Ces bibliothèques fonctionnent sans aucune interaction avec GIL.

Concurrence vs parallélisme
La simultanéité et le parallélisme sont utilisés en relation avec les programmes multithread, mais
il y a beaucoup de confusion sur la similitude et la différence entre eux. La grande question à cet
égard: le parallélisme de concurrence est-il ou non? Bien que les deux termes semblent assez
similaires, mais que la réponse à la question ci-dessus soit NON, la concurrence et le
parallélisme ne sont pas les mêmes. Maintenant, s'ils ne sont pas les mêmes, quelle est la
différence fondamentale entre eux?
En termes simples, la concurrence traite de la gestion de l'accès à l'état partagé à partir de
différents threads et de l'autre côté, le parallélisme traite de l'utilisation de plusieurs processeurs
ou de ses cœurs pour améliorer les performances du matériel.

Concurrence en détail
La concurrence se produit lorsque deux tâches se chevauchent lors de l'exécution. Il peut s'agir
d'une situation où une application progresse sur plus d'une tâche à la fois. Nous pouvons le
comprendre schématiquement; plusieurs tâches progressent en même temps, comme suit -
Niveaux de concurrence
Dans cette section, nous aborderons les trois niveaux importants de concurrence en termes de
programmation -

Concurrence de bas niveau


Dans ce niveau de concurrence, il y a une utilisation explicite des opérations atomiques. Nous ne
pouvons pas utiliser ce type de concurrence pour la création d'applications, car il est très sujet
aux erreurs et difficile à déboguer. Même Python ne prend pas en charge ce type de
concurrence.

Concurrence de niveau intermédiaire


Dans cette concurrence, il n'y a pas d'utilisation d'opérations atomiques explicites. Il utilise les
verrous explicites. Python et d'autres langages de programmation prennent en charge ce type de
concurrence. La plupart des programmeurs d'applications utilisent cette concurrence.

Concurrence de haut niveau


Dans cette concurrence, ni les opérations atomiques explicites ni les verrous explicites ne sont
utilisés. Python aconcurrent.futures module pour prendre en charge ce type de concurrence.

Propriétés des systèmes concurrents


Pour qu'un programme ou un système concurrent soit correct, certaines propriétés doivent être
satisfaites par celui-ci. Les propriétés liées à la terminaison du système sont les suivantes -

Propriété d'exactitude
La propriété d'exactitude signifie que le programme ou le système doit fournir la bonne réponse
souhaitée. Pour faire simple, nous pouvons dire que le système doit correctement mapper l'état
du programme de départ à l'état final.

Propriété de sécurité
La propriété de sécurité signifie que le programme ou le système doit rester dans
un “good” ou “safe” état et ne fait jamais rien “bad”.

Propriété vivante
Cette propriété signifie qu'un programme ou un système doit “make progress” et il atteindrait un
état souhaitable.
Acteurs de systèmes concurrents
Il s'agit d'une propriété commune du système concurrent dans lequel il peut y avoir plusieurs
processus et threads, qui s'exécutent en même temps pour progresser sur leurs propres tâches.
Ces processus et threads sont appelés acteurs du système concurrent.

Ressources des systèmes concurrents


Les acteurs doivent utiliser les ressources telles que la mémoire, le disque, l'imprimante, etc. afin
d'accomplir leurs tâches.

Certains ensembles de règles


Chaque système concurrent doit posséder un ensemble de règles pour définir le type de tâches à
effectuer par les acteurs et le calendrier de chacune. Les tâches peuvent être l'acquisition de
verrous, le partage de mémoire, la modification de l'état, etc.

Obstacles des systèmes concurrents


Lors de la mise en œuvre de systèmes concurrents, le programmeur doit prendre en
considération les deux problèmes importants suivants, qui peuvent être les barrières des
systèmes concurrents:

Partage de données
Un problème important lors de la mise en œuvre des systèmes simultanés est le partage des
données entre plusieurs threads ou processus. En fait, le programmeur doit s'assurer que les
verrous protègent les données partagées afin que tous les accès à celles-ci soient sérialisés et
qu'un seul thread ou processus puisse accéder aux données partagées à la fois. Dans le cas où
plusieurs threads ou processus essaient tous d'accéder aux mêmes données partagées, tous,
mais au moins un d'entre eux, ne seraient pas bloqués et resteraient inactifs. En d'autres termes,
nous pouvons dire que nous ne pourrions utiliser qu'un seul processus ou thread à la fois lorsque
le verrouillage est en vigueur. Il peut y avoir des solutions simples pour supprimer les barrières
mentionnées ci-dessus -

Restriction de partage de données


La solution la plus simple consiste à ne partager aucune donnée modifiable. Dans ce cas, nous
n'avons pas besoin d'utiliser le verrouillage explicite et la barrière de concurrence due aux
données mutuelles serait résolue.

Aide à la structure des données


Plusieurs fois, les processus simultanés doivent accéder aux mêmes données en même temps.
Une autre solution, que l'utilisation de verrous explicites, consiste à utiliser une structure de
données qui prend en charge l'accès simultané. Par exemple, nous pouvons utiliser
lequeuemodule, qui fournit des files d'attente thread-safe. Nous pouvons également
utilisermultiprocessing.JoinableQueue classes pour l'accès concurrentiel basé sur le
multitraitement.
Transfert de données immuable
Parfois, la structure de données que nous utilisons, disons la file d'attente d'accès concurrentiel,
ne convient pas, nous pouvons alors transmettre les données immuables sans les verrouiller.

Transfert de données mutable


Dans le prolongement de la solution ci-dessus, supposons que s'il est nécessaire de ne
transmettre que des données modifiables, plutôt que des données immuables, nous pouvons
alors transmettre des données mutables en lecture seule.

Partage des ressources d'E / S


Un autre problème important dans l'implémentation de systèmes simultanés est l'utilisation des
ressources d'E / S par des threads ou des processus. Le problème survient lorsqu'un thread ou
un processus utilise les E / S pendant si longtemps et qu'un autre est inactif. Nous pouvons voir
ce type de barrière tout en travaillant avec une application lourde d'E / S. Il peut être compris à
l'aide d'un exemple, la demande de pages à partir d'un navigateur Web. C'est une application
lourde. Ici, si la vitesse à laquelle les données sont demandées est plus lente que la vitesse à
laquelle elles sont consommées, alors nous avons une barrière d'E / S dans notre système
concurrent.
Le script Python suivant permet de demander une page Web et d'obtenir le temps que notre
réseau a mis pour obtenir la page demandée -

import urllib.request

import time

ts = time.time()

req = urllib.request.urlopen('http://www.tutorialspoint.com')

pageHtml = req.read()

te = time.time()

print("Page Fetching Time : {} Seconds".format (te-ts))

Après avoir exécuté le script ci-dessus, nous pouvons obtenir le temps de récupération de la
page comme indiqué ci-dessous.

Production
Page Fetching Time: 1.0991398811340332 Seconds
Nous pouvons voir que le temps de récupération de la page est supérieur à une seconde.
Maintenant, que se passe-t-il si nous voulons récupérer des milliers de pages Web différentes,
vous pouvez comprendre combien de temps notre réseau prendrait.

Qu'est-ce que le parallélisme?


Le parallélisme peut être défini comme l'art de diviser les tâches en sous-tâches pouvant être
traitées simultanément. C'est le contraire de la concurrence, comme indiqué ci-dessus, dans
laquelle deux événements ou plus se produisent en même temps. Nous pouvons le comprendre
schématiquement; une tâche est divisée en un certain nombre de sous-tâches qui peuvent être
traitées en parallèle, comme suit -

Pour avoir plus d'idées sur la distinction entre la concurrence et le parallélisme, considérez les
points suivants -

Concurrent mais pas parallèle


Une application peut être concurrente mais pas parallèle signifie qu'elle traite plus d'une tâche à
la fois, mais les tâches ne sont pas divisées en sous-tâches.

Parallèle mais pas simultané


Une application peut être parallèle mais pas concurrente signifie qu'elle ne fonctionne que sur
une tâche à la fois et que les tâches décomposées en sous-tâches peuvent être traitées en
parallèle.

Ni parallèle ni simultané
Une application ne peut être ni parallèle ni concurrente. Cela signifie qu'il ne fonctionne que sur
une seule tâche à la fois et que la tâche n'est jamais divisée en sous-tâches.

Parallèle et simultané
Une application peut être à la fois parallèle et simultanée, ce qui signifie qu'elle fonctionne à la
fois sur plusieurs tâches à la fois et que la tâche est divisée en sous-tâches pour les exécuter en
parallèle.
Nécessité du parallélisme
Nous pouvons réaliser le parallélisme en répartissant les sous-tâches entre différents cœurs d'un
seul processeur ou entre plusieurs ordinateurs connectés au sein d'un réseau.
Considérez les points importants suivants pour comprendre pourquoi il est nécessaire de réaliser
le parallélisme -

Exécution de code efficace


Avec l'aide du parallélisme, nous pouvons exécuter notre code efficacement. Cela nous fera
gagner du temps car le même code en parties s'exécute en parallèle.

Plus rapide que l'informatique séquentielle


Le calcul séquentiel est limité par des facteurs physiques et pratiques en raison desquels il n'est
pas possible d'obtenir des résultats de calcul plus rapides. D'autre part, ce problème est résolu
par le calcul parallèle et nous donne des résultats de calcul plus rapides que le calcul séquentiel.

Moins de temps d'exécution


Le traitement parallèle réduit le temps d'exécution du code du programme.
Si nous parlons d'exemple réel de parallélisme, la carte graphique de notre ordinateur est
l'exemple qui met en évidence la véritable puissance du traitement parallèle car elle possède des
centaines de cœurs de traitement individuels qui fonctionnent indépendamment et peuvent
effectuer l'exécution en même temps. Pour cette raison, nous sommes également en mesure
d'exécuter des applications et des jeux haut de gamme.

Compréhension des processeurs pour la


mise en œuvre
Nous connaissons la concurrence, le parallélisme et la différence entre eux, mais qu'en est-il du
système sur lequel il doit être implémenté. Il est très nécessaire d'avoir la compréhension du
système, sur lequel nous allons mettre en œuvre, car cela nous donne l'avantage de prendre des
décisions éclairées lors de la conception du logiciel. Nous avons les deux types de processeurs
suivants -

Processeurs monocœur
Les processeurs monocœur sont capables d'exécuter un thread à tout moment. Ces processeurs
utilisentcontext switchingpour stocker toutes les informations nécessaires pour un thread à un
moment précis, puis restaurer les informations ultérieurement. Le mécanisme de changement de
contexte nous aide à progresser sur un certain nombre de threads en une seconde donnée et il
semble que le système travaille sur plusieurs choses.
Les processeurs monocœur présentent de nombreux avantages. Ces processeurs nécessitent
moins d'énergie et il n'y a pas de protocole de communication complexe entre plusieurs cœurs.
D'autre part, la vitesse des processeurs monocœur est limitée et ne convient pas aux
applications plus importantes.
Processeurs multicœurs
Les processeurs multicœurs ont plusieurs unités de traitement indépendantes également
appelées cores.
Ces processeurs n'ont pas besoin de mécanisme de changement de contexte car chaque cœur
contient tout ce dont il a besoin pour exécuter une séquence d'instructions stockées.

Cycle d'extraction-décodage-exécution
Les cœurs des processeurs multicœurs suivent un cycle d'exécution. Ce cycle s'appelle leFetch-
Decode-Executecycle. Cela implique les étapes suivantes -

Récupérer
C'est la première étape du cycle, qui implique la récupération des instructions de la mémoire du
programme.

Décoder
Les instructions récemment récupérées seraient converties en une série de signaux qui
déclencheront d'autres parties du processeur.

Exécuter
C'est la dernière étape dans laquelle les instructions extraites et décodées seraient exécutées.
Le résultat de l'exécution sera stocké dans un registre CPU.
Un avantage ici est que l'exécution dans les processeurs multicœurs est plus rapide que celle
des processeurs monocœur. Il convient aux applications plus importantes. D'autre part, un
protocole de communication complexe entre plusieurs cœurs est un problème. Les cœurs
multiples nécessitent plus de puissance que les processeurs monocœur.

Architecture système et mémoire


Il existe différents styles d'architecture de système et de mémoire qui doivent être pris en compte
lors de la conception du programme ou du système simultané. C'est très nécessaire car un
système et un style de mémoire peuvent convenir à une tâche, mais peuvent être sujets à des
erreurs pour une autre tâche.

Architectures de système informatique


prenant en charge la concurrence
Michael Flynn en 1972 a donné une taxonomie pour catégoriser différents styles d'architecture de
système informatique. Cette taxonomie définit quatre styles différents comme suit -
 Flux d'instructions unique, flux de données unique (SISD)
 Flux d'instructions unique, flux de données multiples (SIMD)
 Flux d'instructions multiples, flux de données unique (MISD)
 Flux d'instructions multiples, flux de données multiples (MIMD).
Flux d'instructions unique, flux de
données unique (SISD)
Comme son nom l'indique, ce type de système aurait un flux de données entrant séquentiel et
une seule unité de traitement pour exécuter le flux de données. Ils ressemblent à des systèmes
monoprocesseurs ayant une architecture de calcul parallèle. Voici l'architecture du SISD -

Avantages du SISD
Les avantages de l'architecture SISD sont les suivants -
 Cela nécessite moins d'énergie.
 Il n'y a pas de problème de protocole de communication complexe entre plusieurs cœurs.

Inconvénients du SISD
Les inconvénients de l'architecture SISD sont les suivants -
 La vitesse de l'architecture SISD est limitée, tout comme les processeurs monocœur.
 Il ne convient pas aux applications plus importantes.

Flux d'instructions unique, flux de


données multiples (SIMD)
Comme son nom l'indique, ce type de système aurait plusieurs flux de données entrants et un
nombre d'unités de traitement pouvant agir sur une seule instruction à tout moment. Ils
ressemblent à des systèmes multiprocesseurs ayant une architecture de calcul parallèle. Voici
l'architecture de SIMD -
Le meilleur exemple pour SIMD est les cartes graphiques. Ces cartes ont des centaines d'unités
de traitement individuelles. Si nous parlons de différence de calcul entre SISD et SIMD, alors
pour l'ajout de tableaux[5, 15, 20] et [15, 25, 10],L'architecture SISD devrait effectuer trois
opérations d'ajout différentes. Par contre, avec l'architecture SIMD, on peut alors ajouter en une
seule opération d'ajout.

Avantages de SIMD
Les avantages de l'architecture SIMD sont les suivants -
 La même opération sur plusieurs éléments peut être effectuée en utilisant une seule
instruction.
 Le débit du système peut être augmenté en augmentant le nombre de cœurs du
processeur.
 La vitesse de traitement est supérieure à l'architecture SISD.

Inconvénients de SIMD
Les inconvénients de l'architecture SIMD sont les suivants -
 Il existe une communication complexe entre plusieurs cœurs de processeur.
 Le coût est plus élevé que l'architecture SISD.

Flux de données uniques à instructions


multiples (MISD)
Les systèmes avec flux MISD ont un certain nombre d'unités de traitement effectuant différentes
opérations en exécutant différentes instructions sur le même ensemble de données. Voici
l'architecture de MISD -

Les représentants de l'architecture MISD n'existent pas encore commercialement.

Flux de données multiples à instructions


multiples (MIMD)
Dans le système utilisant l'architecture MIMD, chaque processeur d'un système multiprocesseur
peut exécuter différents ensembles d'instructions indépendamment sur les différents ensembles
de données en parallèle. Elle est opposée à l'architecture SIMD dans laquelle une seule
opération est exécutée sur plusieurs ensembles de données. Voici l'architecture de MIMD -
Un multiprocesseur normal utilise l'architecture MIMD. Ces architectures sont essentiellement
utilisées dans un certain nombre de domaines d'application tels que la conception assistée par
ordinateur / la fabrication assistée par ordinateur, la simulation, la modélisation, les
commutateurs de communication, etc.

Architectures de mémoire prenant en


charge la concurrence
Tout en travaillant avec des concepts tels que la concurrence et le parallélisme, il est toujours
nécessaire d'accélérer les programmes. Une solution trouvée par les concepteurs d'ordinateurs
est de créer des multi-ordinateurs à mémoire partagée, c'est-à-dire des ordinateurs ayant un seul
espace d'adressage physique, auquel tous les cœurs d'un processeur ont accès. Dans ce
scénario, il peut y avoir un certain nombre de styles d'architecture différents, mais voici les trois
styles d'architecture importants:

UMA (accès mémoire uniforme)


Dans ce modèle, tous les processeurs partagent la mémoire physique de manière uniforme. Tous
les processeurs ont le même temps d'accès à tous les mots mémoire. Chaque processeur peut
avoir une mémoire cache privée. Les périphériques suivent un ensemble de règles.
Lorsque tous les processeurs ont un accès égal à tous les périphériques, le système est
appelé symmetric multiprocessor. Lorsque seul un ou quelques processeurs peuvent accéder
aux périphériques, le système est appeléasymmetric multiprocessor.
Accès mémoire non uniforme (NUMA)
Dans le modèle multiprocesseur NUMA, le temps d'accès varie en fonction de l'emplacement du
mot mémoire. Ici, la mémoire partagée est physiquement répartie entre tous les processeurs,
appelés mémoires locales. La collection de toutes les mémoires locales forme un espace
d'adressage global auquel tous les processeurs peuvent accéder.

Architecture de mémoire cache uniquement (COMA)


Le modèle COMA est une version spécialisée du modèle NUMA. Ici, toutes les mémoires
principales distribuées sont converties en mémoires cache.

Concurrence en Python - Threads


En général, comme nous le savons, le fil est une ficelle torsadée très fine généralement du tissu
de coton ou de soie et utilisée pour coudre des vêtements et autres. Le même terme fil est
également utilisé dans le monde de la programmation informatique. Maintenant, comment relier
le fil utilisé pour coudre les vêtements et le fil utilisé pour la programmation informatique? Les
rôles joués par les deux threads sont similaires ici. Dans les vêtements, le fil tient le tissu
ensemble et de l'autre côté, dans la programmation informatique, le fil tient le programme
informatique et permet au programme d'exécuter des actions séquentielles ou de nombreuses
actions à la fois.
Threadest la plus petite unité d'exécution dans un système d'exploitation. Ce n'est pas en soi un
programme mais s'exécute dans un programme. En d'autres termes, les threads ne sont pas
indépendants les uns des autres et partagent une section de code, une section de données, etc.
avec d'autres threads. Ces threads sont également appelés processus légers.

États du fil
Pour comprendre en profondeur la fonctionnalité des threads, nous devons en savoir plus sur le
cycle de vie des threads ou sur les différents états des threads. En règle générale, un thread peut
exister dans cinq états distincts. Les différents états sont indiqués ci-dessous -

Nouveau fil
Un nouveau thread commence son cycle de vie dans le nouvel état. Cependant, à ce stade, il n'a
pas encore démarré et aucune ressource ne lui a été allouée. Nous pouvons dire que ce n'est
qu'une instance d'un objet.

Runnable
Lorsque le thread nouvellement né est démarré, le thread devient exécutable, c'est-à-dire en
attente d'exécution. Dans cet état, il dispose de toutes les ressources mais le planificateur de
tâches n'a toujours pas planifié son exécution.

Fonctionnement
Dans cet état, le thread progresse et exécute la tâche, qui a été choisie par le planificateur de
tâches à exécuter. Maintenant, le thread peut passer soit à l'état mort, soit à l'état non exécutable
/ en attente.

Non en cours d'exécution / en attente


Dans cet état, le thread est suspendu car il attend la réponse d'une demande d'E / S ou attend la
fin de l'exécution d'un autre thread.

Mort
Un thread exécutable entre dans l'état terminé lorsqu'il termine sa tâche ou s'arrête autrement.
Le diagramme suivant montre le cycle de vie complet d'un thread -
Types de fil
Dans cette section, nous verrons les différents types de threads. Les types sont décrits ci-
dessous -

Threads de niveau utilisateur


Ce sont des threads gérés par l'utilisateur.
Dans ce cas, le noyau de gestion des threads n'a pas connaissance de l'existence de threads. La
bibliothèque de threads contient du code pour créer et détruire des threads, pour passer des
messages et des données entre des threads, pour planifier l'exécution de threads et pour
enregistrer et restaurer des contextes de thread. L'application démarre avec un seul thread.
Les exemples de threads de niveau utilisateur sont -
 Fils Java
 Fils POSIX

Avantages des threads de niveau utilisateur


Voici les différents avantages des threads de niveau utilisateur -
 La commutation de thread ne nécessite pas de privilèges de mode noyau.
 Le thread de niveau utilisateur peut s'exécuter sur n'importe quel système d'exploitation.
 La planification peut être spécifique à l'application dans le thread de niveau utilisateur.
 Les threads de niveau utilisateur sont rapides à créer et à gérer.

Inconvénients des threads de niveau utilisateur


Voici les différents inconvénients des threads de niveau utilisateur -
 Dans un système d'exploitation classique, la plupart des appels système sont bloquants.
 L'application multithread ne peut pas tirer parti du multitraitement.

Threads au niveau du noyau


Les threads gérés par le système d'exploitation agissent sur le noyau, qui est un noyau du
système d'exploitation.
Dans ce cas, le noyau fait la gestion des threads. Il n'y a pas de code de gestion des threads
dans la zone d'application. Les threads du noyau sont pris en charge directement par le système
d'exploitation. Toute application peut être programmée pour être multithread. Tous les threads
d'une application sont pris en charge dans un seul processus.
Le noyau conserve les informations de contexte pour le processus dans son ensemble et pour
les threads individuels du processus. La planification par le noyau se fait sur une base de thread.
Le noyau effectue la création, la planification et la gestion des threads dans l'espace noyau. Les
threads du noyau sont généralement plus lents à créer et à gérer que les threads utilisateur. Les
exemples de threads au niveau du noyau sont Windows, Solaris.

Avantages des threads au niveau du noyau


Voici les différents avantages des threads au niveau du noyau -
 Le noyau peut planifier simultanément plusieurs threads du même processus sur
plusieurs processus.
 Si un thread d'un processus est bloqué, le noyau peut planifier un autre thread du même
processus.
 Les routines du noyau elles-mêmes peuvent être multithreads.
Inconvénients des threads au niveau du noyau
 Les threads du noyau sont généralement plus lents à créer et à gérer que les threads
utilisateur.
 Le transfert de contrôle d'un thread à un autre au sein du même processus nécessite un
changement de mode vers le noyau.

Bloc de contrôle de filetage - TCB


Le bloc de contrôle des threads (TCB) peut être défini comme la structure de données du noyau
du système d'exploitation qui contient principalement des informations sur les threads. Les
informations spécifiques aux threads stockées dans TCB mettront en évidence certaines
informations importantes sur chaque processus.
Considérez les points suivants liés aux threads contenus dans TCB -
 Thread identification - C'est l'identifiant de thread unique (tid) attribué à chaque
nouveau thread.
 Thread state - Il contient les informations relatives à l'état (Running, Runnable, Non-
Running, Dead) du thread.
 Program Counter (PC) - Il pointe vers l'instruction de programme actuelle du thread.
 Register set - Il contient les valeurs de registre du thread qui leur sont assignées pour
les calculs.
 Stack Pointer- Il pointe vers la pile du thread dans le processus. Il contient les variables
locales sous la portée du thread.
 Pointer to PCB - Il contient le pointeur vers le processus qui a créé ce thread.

Relation entre processus et thread


En multithreading, processus et thread sont deux termes très étroitement liés ayant le même
objectif de rendre l'ordinateur capable de faire plus d'une chose à la fois. Un processus peut
contenir un ou plusieurs threads mais au contraire, un thread ne peut pas contenir de processus.
Cependant, ils restent tous les deux les deux unités de base de l'exécution. Un programme,
exécutant une série d'instructions, lance le processus et le thread à la fois.
Le tableau suivant montre la comparaison entre processus et thread -
Processus Fil

Le processus est lourd ou gourmand en ressources. Thread est léger et prend m

La commutation de processus nécessite une interaction avec le système La commutation de thread


d'exploitation. système d'exploitation.

Dans plusieurs environnements de traitement, chaque processus exécute le même Tous les threads peuvent pa
code mais possède sa propre mémoire et ses propres ressources de fichiers. ouverts, processus enfants.

Si un processus est bloqué, aucun autre processus ne peut s'exécuter tant que le Pendant qu'un thread est b
premier processus n'est pas débloqué. de la même tâche peut s'ex

Plusieurs processus sans utiliser de threads utilisent plus de ressources. Plusieurs processus threadé

Dans plusieurs processus, chaque processus fonctionne indépendamment des autres. Un thread peut lire, écrire o
thread.

S'il y avait un changement dans le processus parent, cela n'affecte pas les processus S'il y avait une modification
enfants. affecter le comportement d

Pour communiquer avec des processus frères, les processus doivent utiliser la Les threads peuvent commu
communication inter-processus. threads de ce processus.

Concept de multithreading
Comme nous l'avons vu précédemment, le multithreading est la capacité d'un processeur à gérer
l'utilisation du système d'exploitation en exécutant plusieurs threads simultanément. L'idée
principale du multithreading est de réaliser le parallélisme en divisant un processus en plusieurs
threads. De manière plus simple, on peut dire que le multithreading est le moyen de réaliser le
multitâche en utilisant le concept de threads.
Le concept de multithreading peut être compris à l'aide de l'exemple suivant.

Exemple
Supposons que nous exécutions un processus. Le processus pourrait être d'ouvrir MS Word pour
écrire quelque chose. Dans un tel processus, un thread sera assigné pour ouvrir MS word et un
autre thread sera nécessaire pour écrire. Maintenant, supposons que si nous voulons éditer
quelque chose, un autre thread sera nécessaire pour effectuer la tâche d'édition et ainsi de suite.
Le diagramme suivant nous aide à comprendre comment plusieurs threads existent en mémoire -

Nous pouvons voir dans le diagramme ci-dessus que plusieurs threads peuvent exister dans un
processus où chaque thread contient son propre jeu de registres et des variables locales. En
dehors de cela, tous les threads d'un processus partagent des variables globales.

Avantages du multithreading
Voyons maintenant quelques avantages du multithreading. Les avantages sont les suivants -
 Speed of communication - Le multithreading améliore la vitesse de calcul car chaque
cœur ou processeur gère des threads séparés simultanément.
 Program remains responsive - Il permet à un programme de rester réactif car un thread
attend l'entrée et un autre exécute une interface graphique en même temps.
 Access to global variables - En multithreading, tous les threads d'un processus
particulier peuvent accéder aux variables globales et s'il y a un changement dans la
variable globale, il est également visible pour les autres threads.
 Utilization of resources - L'exécution de plusieurs threads dans chaque programme
permet une meilleure utilisation du processeur et le temps d'inactivité du processeur
diminue.
 Sharing of data - Il n'y a pas besoin d'espace supplémentaire pour chaque thread car les
threads d'un programme peuvent partager les mêmes données.

Inconvénients du multithreading
Voyons maintenant quelques inconvénients du multithreading. Les inconvénients sont les
suivants -
 Not suitable for single processor system - Le multithreading a du mal à obtenir des
performances en termes de vitesse de calcul sur un système à processeur unique par
rapport aux performances sur un système multiprocesseur.
 Issue of security - Comme nous savons que tous les threads d'un programme partagent
les mêmes données, il y a donc toujours un problème de sécurité car tout thread inconnu
peut changer les données.
 Increase in complexity - Le multithreading peut augmenter la complexité du programme
et le débogage devient difficile.
 Lead to deadlock state - Le multithreading peut conduire le programme à un risque
potentiel d'atteindre l'état de blocage.
 Synchronization required- La synchronisation est nécessaire pour éviter l'exclusion
mutuelle. Cela conduit à une plus grande utilisation de la mémoire et du processeur.
Implémentation de threads
Dans ce chapitre, nous allons apprendre à implémenter des threads en Python.

Module Python pour l'implémentation des


threads
Les threads Python sont parfois appelés processus légers car les threads occupent beaucoup
moins de mémoire que les processus. Les threads permettent d'effectuer plusieurs tâches à la
fois. En Python, nous avons les deux modules suivants qui implémentent des threads dans un
programme -
 <_thread>module
 <threading>module
La principale différence entre ces deux modules est que <_thread> le module traite un thread
comme une fonction alors que le module <threading>module traite chaque thread comme un
objet et l'implémente de manière orientée objet. De plus, le<_thread>module est efficace dans le
threading de bas niveau et a moins de capacités que le <threading> module.

Module <_thread>
Dans la version précédente de Python, nous avions le <thread>module mais il a été considéré
comme "obsolète" pendant assez longtemps. Les utilisateurs ont été encouragés à utiliser
le<threading>module à la place. Par conséquent, dans Python 3, le module "thread" n'est plus
disponible. Il a été renommé en "<_thread>"pour les incompatibilités vers l'arrière dans Python3.
Pour générer un nouveau fil à l'aide du <_thread> module, nous devons appeler
le start_new_threadméthode de celui-ci. Le fonctionnement de cette méthode peut être compris
à l'aide de la syntaxe suivante -

_thread.start_new_thread ( function, args[, kwargs] )

Ici -
 args est un tuple d'arguments
 kwargs est un dictionnaire facultatif d'arguments de mots clés
Si nous voulons appeler une fonction sans passer d'argument, nous devons utiliser un tuple vide
d'arguments dans args.
Cet appel de méthode retourne immédiatement, le thread enfant démarre et appelle la fonction
avec la liste passée, le cas échéant, d'arguments. Le thread se termine au fur et à mesure que la
fonction retourne.

Exemple
Voici un exemple de génération d'un nouveau thread à l'aide de <_thread>module. Nous
utilisons ici la méthode start_new_thread ().

import _thread
import time
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print ("%s: %s" % ( threadName, time.ctime(time.time()) ))

try:
_thread.start_new_thread( print_time, ("Thread-1", 2, ) )
_thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print ("Error: unable to start thread")
while 1:
pass

Production
La sortie suivante nous aidera à comprendre la génération de nouveaux threads à l'aide
du <_thread> module.

Thread-1: Mon Apr 23 10:03:33 2018


Thread-2: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:37 2018
Thread-2: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:41 2018
Thread-2: Mon Apr 23 10:03:43 2018
Thread-2: Mon Apr 23 10:03:47 2018
Thread-2: Mon Apr 23 10:03:51 2018

Module <threading>
le <threading>module implémente de manière orientée objet et traite chaque thread comme un
objet. Par conséquent, il fournit une prise en charge beaucoup plus puissante et de haut niveau
des threads que le module <_thread>. Ce module est inclus avec Python 2.4.
Méthodes supplémentaires dans le
module <threading>
le <threading> module comprend toutes les méthodes du <_thread>module mais il fournit
également des méthodes supplémentaires. Les méthodes supplémentaires sont les suivantes -
 threading.activeCount() - Cette méthode renvoie le nombre d'objets thread qui sont
actifs
 threading.currentThread() - Cette méthode renvoie le nombre d'objets de thread dans le
contrôle de thread de l'appelant.
 threading.enumerate() - Cette méthode renvoie une liste de tous les objets de thread
actuellement actifs.
Pour implémenter le threading, le <threading> module a le Thread classe qui fournit les
méthodes suivantes -
o run() - La méthode run () est le point d'entrée d'un thread.
o start() - La méthode start () démarre un thread en appelant la méthode run.
o join([time]) - Le join () attend la fin des threads.
o isAlive() - La méthode isAlive () vérifie si un thread est toujours en cours
d'exécution.
o getName() - La méthode getName () renvoie le nom d'un thread.
o setName() - La méthode setName () définit le nom d'un thread.

Comment créer des threads à l'aide du


module <threading>?
Dans cette section, nous allons apprendre à créer des threads en utilisant
le <threading>module. Suivez ces étapes pour créer un nouveau thread à l'aide du module
<threading> -
 Step 1 - Dans cette étape, nous devons définir une nouvelle sous-classe
du Thread classe.
 Step 2 - Ensuite, pour ajouter des arguments supplémentaires, nous devons remplacer
le __init__(self [,args]) méthode.
 Step 3 - Dans cette étape, nous devons remplacer la méthode run (self [, args]) pour
implémenter ce que le thread doit faire au démarrage.
Maintenant, après avoir créé le nouveau Thread sous-classe, nous pouvons en créer une
instance, puis démarrer un nouveau thread en invoquant le start(), qui à son tour appelle
le run() méthode.

Exemple
Considérez cet exemple pour apprendre à générer un nouveau thread en utilisant
le <threading> module.

import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("Starting " + self.name)
print_time(self.name, self.counter, 5)
print ("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1

thread1 = myThread(1, "Thread-1", 1)


thread2 = myThread(2, "Thread-2", 2)

thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")
Starting Thread-1
Starting Thread-2

Production
Maintenant, considérez la sortie suivante -

Thread-1: Mon Apr 23 10:52:09 2018


Thread-1: Mon Apr 23 10:52:10 2018
Thread-2: Mon Apr 23 10:52:10 2018
Thread-1: Mon Apr 23 10:52:11 2018
Thread-1: Mon Apr 23 10:52:12 2018
Thread-2: Mon Apr 23 10:52:12 2018
Thread-1: Mon Apr 23 10:52:13 2018
Exiting Thread-1
Thread-2: Mon Apr 23 10:52:14 2018
Thread-2: Mon Apr 23 10:52:16 2018
Thread-2: Mon Apr 23 10:52:18 2018
Exiting Thread-2
Exiting Main Thread

Programme Python pour divers états de


thread
Il existe cinq états de thread: nouveau, exécutable, en cours d'exécution, en attente et mort.
Parmi ces cinq de ces cinq, nous nous concentrerons principalement sur trois états - en cours
d'exécution, en attente et mort. Un thread obtient ses ressources dans l'état d'exécution, attend
les ressources dans l'état d'attente; la version finale de la ressource, si elle est en cours
d’exécution et acquise, est à l’état mort.
Le programme Python suivant à l'aide des méthodes start (), sleep () et join () montrera comment
un thread est entré en état d'exécution, d'attente et mort respectivement.
Step 1 - Importez les modules nécessaires, <threading> et <time>

import threading
import time

Step 2 - Définissez une fonction qui sera appelée lors de la création d'un thread.

def thread_states():
print("Thread entered in running state")

Step 3 - Nous utilisons la méthode sleep () du module de temps pour faire attendre notre thread
pendant 2 secondes.

time.sleep(2)

Step 4 - Maintenant, nous créons un thread nommé T1, qui prend l'argument de la fonction
définie ci-dessus.

T1 = threading.Thread(target=thread_states)

Step 5- Maintenant, avec l'aide de la fonction start (), nous pouvons démarrer notre thread. Il
produira le message que nous avons défini lors de la définition de la fonction.
T1.start()
Thread entered in running state

Step 6 - Maintenant, nous pouvons enfin tuer le thread avec la méthode join () une fois son
exécution terminée.

T1.join()

Démarrer un thread en Python


En python, nous pouvons démarrer un nouveau thread de différentes manières, mais la plus
simple d'entre elles est de le définir comme une fonction unique. Après avoir défini la fonction,
nous pouvons la passer comme cible pour un nouveauthreading.Threadobjet et ainsi de suite.
Exécutez le code Python suivant pour comprendre le fonctionnement de la fonction -

import threading
import time
import random
def Thread_execution(i):
print("Execution of Thread {} started\n".format(i))
sleepTime = random.randint(1,4)
time.sleep(sleepTime)
print("Execution of Thread {} finished".format(i))
for i in range(4):
thread = threading.Thread(target=Thread_execution, args=(i,))
thread.start()
print("Active Threads:" , threading.enumerate())

Production
Execution of Thread 0 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>]

Execution of Thread 1 started


Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>]

Execution of Thread 2 started


Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>]

Execution of Thread 3 started


Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>,
<Thread(Thread-3579, started 4520)>]
Execution of Thread 0 finished
Execution of Thread 1 finished
Execution of Thread 2 finished
Execution of Thread 3 finished

Fils de démon en Python


Avant d'implémenter les threads de démon en Python, nous devons connaître les threads de
démon et leur utilisation. En termes de calcul, le démon est un processus d'arrière-plan qui gère
les demandes de divers services tels que l'envoi de données, les transferts de fichiers, etc. Il
serait inactif s'il n'est plus nécessaire. La même tâche peut également être effectuée avec l'aide
de threads non démon. Cependant, dans ce cas, le thread principal doit suivre manuellement les
threads non démon. D'un autre côté, si nous utilisons des threads démons, le thread principal
peut complètement oublier cela et il sera tué à la fermeture du thread principal. Un autre point
important à propos des threads démons est que nous pouvons choisir de les utiliser uniquement
pour des tâches non essentielles qui ne nous affecteraient pas si elles ne se terminent pas ou se
font tuer entre les deux. Voici l'implémentation des threads démons en python -

import threading
import time
def nondaemonThread():
print("starting my thread")
time.sleep(8)
print("ending my thread")
def daemonThread():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonThread = threading.Thread(target = nondaemonThread)
daemonThread = threading.Thread(target = daemonThread)
daemonThread.setDaemon(True)
daemonThread.start()
nondaemonThread.start()

Dans le code ci-dessus, il y a deux fonctions à


savoir >nondaemonThread() et >daemonThread(). La première fonction imprime son état et se
met en veille après 8 secondes tandis que la fonction deamonThread () imprime indéfiniment
Hello toutes les 2 secondes. Nous pouvons comprendre la différence entre les threads
nondaemon et daemon à l'aide de la sortie suivante -

Hello

starting my thread
Hello
Hello
Hello
Hello
ending my thread
Hello
Hello
Hello
Hello
Hello

Synchronisation des threads


La synchronisation des threads peut être définie comme une méthode à l'aide de laquelle nous
pouvons être assurés que deux ou plusieurs threads simultanés n'accèdent pas simultanément
au segment de programme appelé section critique. D'autre part, comme nous le savons, cette
section critique est la partie du programme où la ressource partagée est accessible. Par
conséquent, nous pouvons dire que la synchronisation est le processus qui consiste à s'assurer
que deux ou plusieurs threads ne s'interfacent pas en accédant aux ressources en même temps.
Le diagramme ci-dessous montre que quatre threads tentent d'accéder à la section critique d'un
programme en même temps.

Pour clarifier les choses, supposons que deux threads ou plus essaient d'ajouter l'objet dans la
liste en même temps. Cet acte ne peut pas conduire à une fin réussie car soit il supprimera un ou
tous les objets, soit il corrompra complètement l'état de la liste. Ici, le rôle de la synchronisation
est qu'un seul thread à la fois peut accéder à la liste.

Problèmes de synchronisation des


threads
Nous pourrions rencontrer des problèmes lors de l'implémentation de la programmation
simultanée ou de l'application de primitives de synchronisation. Dans cette section, nous
aborderons deux problèmes majeurs. Les problèmes sont -
 Deadlock
 Condition de course

Condition de course
C'est l'un des problèmes majeurs de la programmation simultanée. L'accès simultané aux
ressources partagées peut conduire à une condition de concurrence. Une condition de
concurrence peut être définie comme la survenance d'une condition lorsque deux ou plusieurs
threads peuvent accéder aux données partagées et ensuite essayer de modifier sa valeur en
même temps. Pour cette raison, les valeurs des variables peuvent être imprévisibles et varier en
fonction des horaires des changements de contexte des processus.

Exemple
Considérez cet exemple pour comprendre le concept de condition de race -
Step 1 - Dans cette étape, nous devons importer le module de threading -

import threading

Step 2 - Maintenant, définissez une variable globale, disons x, avec sa valeur comme 0 -
x = 0

Step 3 - Maintenant, nous devons définir le increment_global() fonction, qui fera


l'incrémentation de 1 dans cette fonction globale x -

def increment_global():

global x
x += 1

Step 4 - Dans cette étape, nous définirons le taskofThread()function, qui appellera la fonction
increment_global () un certain nombre de fois; pour notre exemple, c'est 50000 fois -

def taskofThread():

for _ in range(50000):
increment_global()

Step 5- Maintenant, définissez la fonction main () dans laquelle les threads t1 et t2 sont créés.
Les deux seront démarrés à l'aide de la fonction start () et attendront de terminer leur travail à
l'aide de la fonction join ().

def main():
global x
x = 0

t1 = threading.Thread(target= taskofThread)
t2 = threading.Thread(target= taskofThread)

t1.start()
t2.start()

t1.join()
t2.join()

Step 6- Maintenant, nous devons donner la plage comme pour le nombre d'itérations que nous
voulons appeler la fonction main (). Ici, nous l'appelons 5 fois.

if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Dans la sortie montrée ci-dessous, nous pouvons voir l'effet de la condition de concurrence
comme la valeur de x après chaque itération est attendue 100000. Cependant, il y a beaucoup
de variation dans la valeur. Cela est dû à l'accès simultané des threads à la variable globale
partagée x.

Production
x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4

Faire face à une condition de concurrence


à l'aide de serrures
Comme nous avons vu l'effet de la condition de concurrence dans le programme ci-dessus, nous
avons besoin d'un outil de synchronisation, qui peut gérer la condition de concurrence entre
plusieurs threads. En Python, le<threading>Le module fournit une classe de verrouillage pour
gérer les conditions de concurrence. De plus, leLockclass fournit différentes méthodes à l'aide
desquelles nous pouvons gérer les conditions de concurrence entre plusieurs threads. Les
méthodes sont décrites ci-dessous -

Acquérir () méthode
Cette méthode est utilisée pour acquérir, c'est-à-dire bloquer un verrou. Un verrou peut être
bloquant ou non bloquant selon la valeur vraie ou fausse suivante -
 With value set to True - Si la méthode Acquérir () est appelée avec True, qui est
l'argument par défaut, alors l'exécution du thread est bloquée jusqu'à ce que le verrou
soit déverrouillé.
 With value set to False - Si la méthode Acquérir () est invoquée avec False, ce qui n'est
pas l'argument par défaut, alors l'exécution du thread n'est pas bloquée tant qu'elle n'est
pas mise à true, c'est-à-dire tant qu'elle n'est pas verrouillée.

méthode release ()
Cette méthode est utilisée pour libérer un verrou. Voici quelques tâches importantes liées à cette
méthode -
 Si un verrou est verrouillé, le release()la méthode le déverrouillerait. Son travail est de
permettre à exactement un thread de continuer si plusieurs threads sont bloqués et
attendent que le verrou soit déverrouillé.
 Cela soulèvera un ThreadError si le verrou est déjà déverrouillé.
Maintenant, nous pouvons réécrire le programme ci-dessus avec la classe de verrouillage et ses
méthodes pour éviter la condition de concurrence. Nous devons définir la méthode taskofThread
() avec l'argument lock, puis utiliser les méthodes Acquérir () et Release () pour bloquer et non
bloquer les verrous afin d'éviter les conditions de concurrence.
Exemple
Voici un exemple de programme python pour comprendre le concept de verrous pour gérer les
conditions de concurrence -

import threading

x = 0

def increment_global():

global x
x += 1

def taskofThread(lock):

for _ in range(50000):
lock.acquire()
increment_global()
lock.release()

def main():
global x
x = 0

lock = threading.Lock()
t1 = threading.Thread(target = taskofThread, args = (lock,))
t2 = threading.Thread(target = taskofThread, args = (lock,))

t1.start()
t2.start()

t1.join()
t2.join()

if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))

La sortie suivante montre que l'effet de la condition de concurrence est négligé; car la valeur de
x, après chaque & chaque itération, est maintenant de 100000, ce qui correspond aux attentes
de ce programme.

Production
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4

Deadlocks - Le problème des philosophes


de la restauration
Le blocage est un problème difficile auquel on peut faire face lors de la conception des systèmes
simultanés. Nous pouvons illustrer ce problème à l'aide du problème du philosophe culinaire
comme suit -
Edsger Dijkstra a à l'origine introduit le problème du philosophe de la restauration, l'une des
illustrations célèbres de l'un des plus gros problème du système concurrent appelé blocage.
Dans ce problème, il y a cinq philosophes célèbres assis à une table ronde en train de manger
de la nourriture dans leurs bols. Il y a cinq fourchettes qui peuvent être utilisées par les cinq
philosophes pour manger leur nourriture. Cependant, les philosophes décident d'utiliser deux
fourchettes en même temps pour manger leur nourriture.
Or, il y a deux conditions principales pour les philosophes. Premièrement, chacun des
philosophes peut être soit en état de manger, soit en état de pensée et deuxièmement, ils doivent
d'abord obtenir les deux fourchettes, c'est-à-dire la gauche et la droite. Le problème se pose
lorsque chacun des cinq philosophes parvient à choisir la fourche gauche en même temps.
Maintenant, ils attendent tous que la bonne fourchette soit libre, mais ils ne renonceront jamais à
leur fourchette tant qu'ils n'auront pas mangé leur nourriture et la bonne fourchette ne sera jamais
disponible. Par conséquent, il y aurait une impasse à la table du dîner.

Blocage dans le système simultané


Maintenant, si nous voyons, le même problème peut également survenir dans nos systèmes
concurrents. Les fourchettes dans l'exemple ci-dessus seraient les ressources du système et
chaque philosophe peut représenter le processus, qui est en concurrence pour obtenir les
ressources.
Solution avec programme Python
La solution de ce problème peut être trouvée en divisant les philosophes en deux types - greedy
philosophers et generous philosophers. Un philosophe avide essaiera principalement de
prendre la fourche gauche et d'attendre qu'elle soit là. Il attendra alors que la bonne fourchette
soit là, la ramasse, la mange et la pose ensuite. D'un autre côté, un philosophe généreux
essaiera de ramasser la fourche gauche et si ce n'est pas là, il attendra et réessayera après un
certain temps. S'ils obtiennent la fourche gauche, ils essaieront d'obtenir la bonne. S'ils
obtiennent également la bonne fourchette, ils mangeront et relâcheront les deux fourchettes.
Cependant, s'ils n'obtiennent pas la fourche droite, ils relâcheront la fourche gauche.

Exemple
Le programme Python suivant nous aidera à trouver une solution au problème du philosophe de
la restauration -

import threading
import random
import time

class DiningPhilosopher(threading.Thread):

running = True

def __init__(self, xname, Leftfork, Rightfork):


threading.Thread.__init__(self)
self.name = xname
self.Leftfork = Leftfork
self.Rightfork = Rightfork

def run(self):
while(self.running):
time.sleep( random.uniform(3,13))
print ('%s is hungry.' % self.name)
self.dine()

def dine(self):
fork1, fork2 = self.Leftfork, self.Rightfork

while self.running:
fork1.acquire(True)
locked = fork2.acquire(False)
if locked: break
fork1.release()
print ('%s swaps forks' % self.name)
fork1, fork2 = fork2, fork1
else:
return

self.dining()
fork2.release()
fork1.release()

def dining(self):
print ('%s starts eating '% self.name)
time.sleep(random.uniform(1,10))
print ('%s finishes eating and now thinking.' % self.name)

def Dining_Philosophers():
forks = [threading.Lock() for n in range(5)]
philosopherNames = ('1st','2nd','3rd','4th', '5th')

philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5],


forks[(i+1)%5]) \
for i in range(5)]

random.seed()
DiningPhilosopher.running = True
for p in philosophers: p.start()
time.sleep(30)
DiningPhilosopher.running = False
print (" It is finishing.")

Dining_Philosophers()

Le programme ci-dessus utilise le concept de philosophes avides et généreux. Le programme a


également utilisé leacquire() et release() méthodes de la Lock classe de la <threading>module.
Nous pouvons voir la solution dans la sortie suivante -
Production
4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.

Intercommunication de threads
Dans la vraie vie, si une équipe de personnes travaille sur une tâche commune, il devrait y avoir
une communication entre elles pour terminer la tâche correctement. La même analogie
s'applique également aux threads. En programmation, pour réduire le temps idéal du processeur,
nous créons plusieurs threads et assignons différentes sous-tâches à chaque thread. Par
conséquent, il doit y avoir une installation de communication et ils doivent interagir les uns avec
les autres pour terminer le travail de manière synchronisée.
Considérez les points importants suivants liés à l'intercommunication de thread -
 No performance gain - Si nous ne pouvons pas parvenir à une communication correcte
entre les threads et les processus, les gains de performances de la concurrence et du
parallélisme ne sont d'aucune utilité.
 Accomplish task properly - Sans mécanisme d'intercommunication approprié entre les
threads, la tâche assignée ne peut pas être exécutée correctement.
 More efficient than inter-process communication - La communication inter-thread est
plus efficace et plus facile à utiliser que la communication inter-processus car tous les
threads d'un processus partagent le même espace d'adressage et n'ont pas besoin
d'utiliser la mémoire partagée.

Structures de données Python pour une


communication thread-safe
Le code multithread pose un problème de transmission d'informations d'un thread à un autre
thread. Les primitives de communication standard ne résolvent pas ce problème. Par
conséquent, nous devons implémenter notre propre objet composite afin de partager des objets
entre les threads pour rendre la communication thread-safe. Voici quelques structures de
données, qui fournissent une communication thread-safe après y avoir apporté quelques
modifications -

Ensembles
Pour utiliser la structure de données set de manière thread-safe, nous devons étendre la classe
set pour implémenter notre propre mécanisme de verrouillage.

Exemple
Voici un exemple Python d'extension de la classe -

class extend_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(extend_class, self).__init__(*args, **kwargs)

def add(self, elem):


self._lock.acquire()
try:
super(extend_class, self).add(elem)
finally:
self._lock.release()
def delete(self, elem):
self._lock.acquire()
try:
super(extend_class, self).delete(elem)
finally:
self._lock.release()

Dans l'exemple ci-dessus, un objet de classe nommé extend_class a été défini qui est hérité du
Python set class. Un objet de verrouillage est créé dans le constructeur de cette classe.
Maintenant, il y a deux fonctions -add() et delete(). Ces fonctions sont définies et sont thread-
safe. Ils comptent tous deux sur lesuper fonctionnalité de classe avec une exception clé.

Décorateur
C'est une autre méthode clé pour la communication thread-safe est l'utilisation de décorateurs.

Exemple
Prenons un exemple Python qui montre comment utiliser les décorateurs & mminus;

def lock_decorator(method):

def new_deco_method(self, *args, **kwargs):


with self._lock:
return method(self, *args, **kwargs)
return new_deco_method

class Decorator_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(Decorator_class, self).__init__(*args, **kwargs)

@lock_decorator
def add(self, *args, **kwargs):
return super(Decorator_class, self).add(elem)
@lock_decorator
def delete(self, *args, **kwargs):
return super(Decorator_class, self).delete(elem)
Dans l'exemple ci-dessus, une méthode décoratrice nommée lock_decorator a été définie,
héritée de la classe de méthode Python. Ensuite, un objet de verrouillage est créé dans le
constructeur de cette classe. Maintenant, il y a deux fonctions - add () et delete (). Ces fonctions
sont définies et sont thread-safe. Ils s'appuient tous deux sur des fonctionnalités de grande
classe à une exception près.

Listes
La structure des données de la liste est sécurisée pour les threads, rapide et simple pour un
stockage temporaire en mémoire. En Cpython, le GIL protège contre l'accès simultané à ceux-ci.
Comme nous avons appris que les listes sont thread-safe, mais qu'en est-il des données qu'elles
contiennent. En fait, les données de la liste ne sont pas protégées. Par
exemple,L.append(x)n'est pas garanti de renvoyer le résultat attendu si un autre thread essaie
de faire la même chose. C'est parce que, bien queappend() est une opération atomique et
thread-safe, mais l'autre thread essaie de modifier les données de la liste de manière
concurrente, nous pouvons donc voir les effets secondaires des conditions de concurrence sur la
sortie.
Pour résoudre ce type de problème et modifier en toute sécurité les données, nous devons
implémenter un mécanisme de verrouillage approprié, ce qui garantit en outre que plusieurs
threads ne peuvent pas potentiellement rencontrer des conditions de concurrence. Pour
implémenter un mécanisme de verrouillage approprié, nous pouvons étendre la classe comme
nous l'avons fait dans les exemples précédents.
Certaines autres opérations atomiques sur les listes sont les suivantes -

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

Ici -
 L, L1, L2 sont tous des listes
 D, D1, D2 sont des dictionnaires
 x, y sont des objets
 je, j sont des entiers

Files d'attente
Si les données de la liste ne sont pas protégées, nous pourrions être confrontés aux
conséquences. Nous pouvons obtenir ou supprimer des données erronées, des conditions de
course. C'est pourquoi il est recommandé d'utiliser la structure de données de file d'attente. Un
exemple concret de file d'attente peut être une route à sens unique à voie unique, où le véhicule
entre en premier, sort en premier. D'autres exemples concrets peuvent être vus des files d'attente
aux guichets et aux arrêts de bus.

Les files d'attente sont par défaut une structure de données thread-safe et nous n'avons pas à
nous soucier de l'implémentation d'un mécanisme de verrouillage complexe. Python nous fournit
le module pour utiliser différents types de files d'attente dans notre application.

Types de files d'attente


Dans cette section, nous découvrirons les différents types de files d'attente. Python fournit trois
options de files d'attente à utiliser à partir du<queue> module -
 Files d'attente normales (FIFO, premier entré, premier sorti)
 LIFO, dernier entré, premier sorti
 Priority
Nous en apprendrons davantage sur les différentes files d'attente dans les sections suivantes.

Files d'attente normales (FIFO, premier


entré, premier sorti)
Il s'agit des implémentations de file d'attente les plus couramment utilisées proposées par
Python. Dans ce mécanisme de mise en file d'attente, quiconque viendra en premier obtiendra le
service en premier. FIFO est également appelé files d'attente normales. Les files d'attente FIFO
peuvent être représentées comme suit -

Implémentation Python de la file d'attente FIFO


En python, la file d'attente FIFO peut être implémentée avec un seul thread ainsi que des
multithreads.
File d'attente FIFO avec un seul thread
Pour implémenter la file d'attente FIFO avec un seul thread, le Queueclass implémentera un
conteneur de base premier entré, premier sorti. Les éléments seront ajoutés à une «extrémité»
de la séquence en utilisantput(), et supprimé de l'autre extrémité en utilisant get().

Exemple
Voici un programme Python pour l'implémentation de la file d'attente FIFO avec un seul thread -

import queue

q = queue.Queue()

for i in range(8):
q.put("item-" + str(i))

while not q.empty():


print (q.get(), end = " ")

Production
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7

La sortie montre que le programme ci-dessus utilise un seul thread pour illustrer que les éléments
sont supprimés de la file d'attente dans le même ordre qu'ils sont insérés.

File d'attente FIFO avec plusieurs threads


Pour implémenter FIFO avec plusieurs threads, nous devons définir la fonction myqueue (), qui
est étendue à partir du module queue. Le fonctionnement des méthodes get () et put () est le
même que celui décrit ci-dessus lors de l'implémentation de la file d'attente FIFO avec un seul
thread. Ensuite, pour le rendre multithread, nous devons déclarer et instancier les threads. Ces
threads consommeront la file d'attente de manière FIFO.

Exemple
Voici un programme Python pour l'implémentation de la file d'attente FIFO avec plusieurs threads

import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(),
item))
queue.task_done()
time.sleep(2)
q = queue.Queue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()

Production
<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue

LIFO, file d'attente Last in First Out


Cette file d'attente utilise une analogie totalement opposée aux files d'attente FIFO (First in First
Out). Dans ce mécanisme de mise en file d'attente, celui qui vient en dernier obtiendra le service
en premier. Ceci est similaire à l'implémentation de la structure de données de la pile. Les files
d'attente LIFO s'avèrent utiles lors de la mise en œuvre de la recherche en profondeur d'abord
comme des algorithmes d'intelligence artificielle.
Implémentation Python de la file d'attente LIFO
En python, la file d'attente LIFO peut être implémentée avec un seul thread ainsi que des
multithreads.

File d'attente LIFO avec un seul thread


Pour implémenter la file d'attente LIFO avec un seul thread, le Queue class implémentera un
conteneur de base dernier entré, premier sorti en utilisant la structure Queue.LifoQueue.
Maintenant, en appelantput(), les éléments sont ajoutés dans la tête du récipient et retirés de la
tête également en utilisant get().

Exemple
Voici un programme Python pour l'implémentation de la file d'attente LIFO avec un seul thread -

import queue

q = queue.LifoQueue()

for i in range(8):
q.put("item-" + str(i))

while not q.empty():


print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0

La sortie montre que le programme ci-dessus utilise un seul thread pour illustrer que les éléments
sont supprimés de la file d'attente dans l'ordre inverse de leur insertion.

File d'attente LIFO avec plusieurs threads


L'implémentation est similaire à celle que nous avons réalisée avec l'implémentation de files
d'attente FIFO avec plusieurs threads. La seule différence est que nous devons utiliser
leQueue classe qui implémentera un conteneur de base dernier entré, premier sorti en utilisant la
structure Queue.LifoQueue.

Exemple
Voici un programme Python pour l'implémentation de la file d'attente LIFO avec plusieurs threads
-

import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the
queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()

Production
<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue

File d'attente de priorité


Dans les files d'attente FIFO et LIFO, l'ordre des éléments est lié à l'ordre d'insertion. Cependant,
il existe de nombreux cas où la priorité est plus importante que l'ordre d'insertion. Prenons un
exemple du monde réel. Supposons que la sécurité à l'aéroport vérifie les personnes de
différentes catégories. Les personnes du VVIP, le personnel de la compagnie aérienne, les
douaniers, les catégories peuvent être vérifiées en priorité au lieu d'être vérifiées sur la base de
l'arrivée comme c'est le cas pour les roturiers.
Un autre aspect important à prendre en compte pour la file d'attente prioritaire est la manière de
développer un planificateur de tâches. Une conception courante consiste à servir la tâche la plus
d'agent en priorité dans la file d'attente. Cette structure de données peut être utilisée pour
récupérer les éléments de la file d'attente en fonction de leur valeur de priorité.

Implémentation Python de la file d'attente prioritaire


En python, la file d'attente prioritaire peut être implémentée avec un seul thread ainsi que des
multithreads.

File d'attente prioritaire avec un seul thread


Pour implémenter la file d'attente prioritaire avec un seul thread, le Queue class implémentera
une tâche sur le conteneur prioritaire en utilisant la structure Queue.File d'attente de priorité.
Maintenant, en appelantput(), les éléments sont ajoutés avec une valeur où la valeur la plus
basse aura la priorité la plus élevée et donc récupérés en premier en utilisant get().

Exemple
Considérez le programme Python suivant pour l'implémentation de la file d'attente prioritaire avec
un seul thread -

import queue as Q
p_queue = Q.PriorityQueue()

p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))

while not p_queue.empty():


item = p_queue.get()
print('%s - %s' % item)

Production
1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important
Dans la sortie ci-dessus, nous pouvons voir que la file d'attente a stocké les éléments en fonction
de la priorité - moins de valeur a une priorité élevée.

File d'attente prioritaire avec plusieurs threads


L'implémentation est similaire à l'implémentation des files d'attente FIFO et LIFO avec plusieurs
threads. La seule différence est que nous devons utiliser leQueue classe pour initialiser la priorité
en utilisant la structure Queue.PriorityQueue. Une autre différence concerne la façon dont la file
d'attente serait générée. Dans l'exemple ci-dessous, il sera généré avec deux ensembles de
données identiques.

Exemple
Le programme Python suivant aide à l'implémentation de la file d'attente prioritaire avec plusieurs
threads -

import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the
queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
q.put(i,1)

for i in range(5):
q.put(i,1)

threads = []
for i in range(2):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()

Production
<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue

Test des applications de filetage


Dans ce chapitre, nous allons découvrir comment tester les applications de thread. Nous
apprendrons également l'importance des tests.

Pourquoi tester?
Avant de plonger dans la discussion sur l'importance des tests, nous devons savoir ce que sont
les tests. En termes généraux, les tests sont une technique permettant de déterminer si quelque
chose fonctionne bien. D'un autre côté, en particulier si nous parlons de programmes
informatiques ou de logiciels, le test est la technique d'accès à la fonctionnalité d'un logiciel.
Dans cette section, nous discuterons de l'importance des tests logiciels. Dans le développement
de logiciels, il doit y avoir une double vérification avant la publication du logiciel au client. C'est
pourquoi il est très important de tester le logiciel par une équipe de test expérimentée. Tenez
compte des points suivants pour comprendre l'importance des tests logiciels -

Amélioration de la qualité des logiciels


Certes, aucune entreprise ne souhaite fournir des logiciels de mauvaise qualité et aucun client ne
souhaite acheter de logiciels de mauvaise qualité. Les tests améliorent la qualité du logiciel en
trouvant et en corrigeant les bogues.

Satisfaction des clients


La partie la plus importante de toute entreprise est la satisfaction de ses clients. En fournissant
un logiciel sans bogue et de bonne qualité, les entreprises peuvent satisfaire leurs clients.
Réduisez l'impact des nouvelles fonctionnalités
Supposons que nous ayons créé un système logiciel de 10000 lignes et que nous ayons besoin
d'ajouter une nouvelle fonctionnalité, l'équipe de développement aurait alors des inquiétudes
quant à l'impact de cette nouvelle fonctionnalité sur l'ensemble du logiciel. Ici aussi, les tests
jouent un rôle essentiel car si l'équipe de test a réalisé une bonne suite de tests, cela peut nous
éviter d'éventuelles ruptures catastrophiques.

Expérience utilisateur
Une autre partie la plus importante de toute entreprise est l'expérience des utilisateurs de ce
produit. Seuls les tests peuvent garantir que l'utilisateur final trouve qu'il est simple et facile
d'utiliser le produit.

Réduire les dépenses


Les tests peuvent réduire le coût total du logiciel en trouvant et en corrigeant les bogues en
phase de test de son développement plutôt qu'en les corrigeant après la livraison. S'il y avait un
bogue majeur après la livraison du logiciel, cela augmenterait son coût tangible, par exemple en
termes de dépenses et de coût intangible, par exemple en termes d'insatisfaction client, de
réputation négative de l'entreprise, etc.

Que tester?
Il est toujours recommandé d'avoir une connaissance appropriée de ce qui doit être testé. Dans
cette section, nous allons d'abord comprendre le principal motif du testeur lors du test d'un
logiciel. La couverture de code, c'est-à-dire le nombre de lignes de code que notre suite de tests
atteint, lors des tests, doit être évitée. C'est parce que, lors des tests, se concentrer uniquement
sur le nombre de lignes de codes n'ajoute aucune valeur réelle à notre système. Il peut rester
quelques bogues, qui se reflètent plus tard à un stade ultérieur, même après le déploiement.
Tenez compte des points importants suivants relatifs à ce qu'il faut tester -
 Nous devons nous concentrer sur le test de la fonctionnalité du code plutôt que sur la
couverture du code.
 Nous devons d'abord tester les parties les plus importantes du code, puis passer aux
parties les moins importantes du code. Cela fera certainement gagner du temps.
 Le testeur doit avoir une multitude de tests différents qui peuvent pousser le logiciel à ses
limites.

Approches pour tester les programmes


logiciels simultanés
En raison de la capacité d'utiliser la véritable capacité de l'architecture multicœur, les systèmes
logiciels simultanés remplacent les systèmes séquentiels. Ces derniers temps, des programmes
système simultanés sont utilisés dans tout, des téléphones mobiles aux machines à laver, des
voitures aux avions, etc. déjà un bogue, alors nous nous retrouverions avec plusieurs bogues.
Les techniques de test pour les programmes logiciels simultanés se concentrent largement sur la
sélection de l'entrelacement qui expose des modèles potentiellement dangereux tels que les
conditions de course, les blocages et la violation de l'atomicité. Voici deux approches pour tester
des programmes logiciels simultanés -
Exploration systématique
Cette approche vise à explorer l'espace des entrelacements le plus largement possible. De telles
approches peuvent adopter une technique de force brute et d'autres adoptent une technique de
réduction d'ordre partiel ou une technique heuristique pour explorer l'espace des entrelacements.

Axé sur la propriété


Les approches basées sur les propriétés reposent sur l'observation que les erreurs de
concurrence sont plus susceptibles de se produire sous des entrelacements qui exposent des
propriétés spécifiques telles que des modèles d'accès à la mémoire suspects. Différentes
approches axées sur les propriétés ciblent différentes erreurs telles que les conditions de
concurrence, les blocages et la violation de l'atomicité, qui dépendent en outre de l'une ou
d'autres propriétés spécifiques.

Stratégies de test
La stratégie de test est également appelée approche de test. La stratégie définit la manière dont
les tests seraient effectués. L'approche de test a deux techniques -

Proactif
Une approche dans laquelle le processus de conception de test est lancé le plus tôt possible afin
de trouver et de corriger les défauts avant la création de la construction.

Réactif
Une approche dans laquelle les tests ne commencent qu'à la fin du processus de
développement.
Avant d'appliquer une stratégie ou une approche de test sur un programme python, nous devons
avoir une idée de base sur le type d'erreurs qu'un programme logiciel peut avoir. Les erreurs sont
les suivantes -

Erreurs de syntaxe
Pendant le développement du programme, il peut y avoir de nombreuses petites erreurs. Les
erreurs sont principalement dues à des fautes de frappe. Par exemple, deux points manquants
ou une mauvaise orthographe d'un mot-clé, etc. Ces erreurs sont dues à une erreur de syntaxe
du programme et non de logique. Par conséquent, ces erreurs sont appelées erreurs
syntaxiques.

Erreurs sémantiques
Les erreurs sémantiques sont également appelées erreurs logiques. S'il y a une erreur logique ou
sémantique dans le programme logiciel, l'instruction sera compilée et exécutée correctement
mais elle ne donnera pas la sortie souhaitée car la logique n'est pas correcte.
Test unitaire
C'est l'une des stratégies de test les plus utilisées pour tester les programmes python. Cette
stratégie est utilisée pour tester les unités ou les composants du code. Par unités ou
composants, nous entendons des classes ou des fonctions du code. Les tests unitaires
simplifient les tests de grands systèmes de programmation en testant de «petites» unités. Avec
l'aide du concept ci-dessus, les tests unitaires peuvent être définis comme une méthode dans
laquelle des unités individuelles de code source sont testées pour déterminer si elles renvoient la
sortie souhaitée.
Dans nos sections suivantes, nous découvrirons les différents modules Python pour les tests
unitaires.

module unittest
Le tout premier module pour les tests unitaires est le module unittest. Il est inspiré de JUnit et
inclus par défaut dans Python3.6. Il prend en charge l'automatisation des tests, le partage du
code de configuration et d'arrêt pour les tests, l'agrégation des tests en collections et
l'indépendance des tests par rapport au cadre de reporting.
Voici quelques concepts importants pris en charge par le module unittest

Fixation de texte
Il est utilisé pour configurer un test afin qu'il puisse être exécuté avant de démarrer le test et
démonté après la fin du test. Cela peut impliquer la création d'une base de données temporaire,
de répertoires, etc. nécessaires avant de démarrer le test.

Cas de test
Le cas de test vérifie si une réponse requise provient de l'ensemble spécifique d'entrées ou non.
Le module unittest comprend une classe de base nommée TestCase qui peut être utilisée pour
créer de nouveaux cas de test. Il comprend deux méthodes par défaut -
 setUp()- une méthode de crochet pour installer le dispositif d'essai avant de l'exercer.
Ceci est appelé avant d'appeler les méthodes de test implémentées.
 tearDown( - une méthode hook pour déconstruire le fixture de classe après avoir exécuté
tous les tests de la classe.

Suite de tests
Il s'agit d'un ensemble de suites de tests, de cas de test ou les deux.

Testeur
Il contrôle l'exécution des cas de test ou des combinaisons et fournit le résultat à l'utilisateur. Il
peut utiliser une interface graphique ou une simple interface de texte pour fournir le résultat.
Example
Le programme Python suivant utilise le module unittest pour tester un module nommé Fibonacci.
Le programme aide à calculer la série de Fibonacci d'un nombre. Dans cet exemple, nous avons
créé une classe nommée Fibo_test, pour définir les cas de test en utilisant différentes méthodes.
Ces méthodes sont héritées de unittest.TestCase. Nous utilisons deux méthodes par défaut -
setUp () et tearDown (). Nous définissons également la méthode testfibocal. Le nom du test doit
commencer par le test des lettres. Dans le dernier bloc, unittest.main () fournit une interface de
ligne de commande au script de test.

import unittest
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
class Fibo_Test(unittest.TestCase):
def setUp(self):
print("This is run before our tests would be executed")
def tearDown(self):
print("This is run after the completion of execution of our tests")

def testfibocal(self):
self.assertEqual(fib(0), 0)
self.assertEqual(fib(1), 1)
self.assertEqual(fib(5), 5)
self.assertEqual(fib(10), 55)
self.assertEqual(fib(20), 6765)

if __name__ == "__main__":
unittest.main()

Lorsqu'il est exécuté à partir de la ligne de commande, le script ci-dessus produit une sortie qui
ressemble à ceci -

Production
This runs before our tests would be executed.
This runs after the completion of execution of our tests.
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Maintenant, pour être plus clair, nous changeons notre code qui a aidé à définir le module
Fibonacci.
Considérez le bloc de code suivant comme exemple -

def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a

Quelques modifications au bloc de code sont apportées comme indiqué ci-dessous -

def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a

Maintenant, après avoir exécuté le script avec le code modifié, nous obtiendrons la sortie
suivante -

This runs before our tests would be executed.


This runs after the completion of execution of our tests.
F
======================================================================
FAIL: testCalculation (__main__.Fibo_Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unitg.py", line 15, in testCalculation
self.assertEqual(fib(0), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.007s

FAILED (failures = 1)

La sortie ci-dessus montre que le module n'a pas réussi à fournir la sortie souhaitée.

Module Docktest
Le module docktest aide également aux tests unitaires. Il est également livré pré-emballé avec
python. Il est plus facile à utiliser que le module unittest. Le module unittest est plus adapté aux
tests complexes. Pour utiliser le module doctest, nous devons l'importer. La docstring de la
fonction correspondante doit avoir une session python interactive avec leurs sorties.
Si tout va bien dans notre code, il n'y aura pas de sortie du module docktest; sinon, il fournira la
sortie.

Exemple
L'exemple Python suivant utilise le module docktest pour tester un module nommé Fibonacci, qui
aide à calculer la série Fibonacci d'un nombre.

import doctest
def fibonacci(n):
"""
Calculates the Fibonacci number

>>> fibonacci(0)
0
>>> fibonacci(1)
1
>>> fibonacci(10)
55
>>> fibonacci(20)
6765
>>>

"""
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
if __name__ == "__main__":
doctest.testmod()

Nous pouvons voir que la docstring de la fonction correspondante nommée fib avait une session
python interactive avec les sorties. Si notre code est correct, il n'y aurait pas de sortie du module
doctest. Mais pour voir comment cela fonctionne, nous pouvons l'exécuter avec l'option –v.

(base) D:\ProgramData>python dock_test.py -v


Trying:
fibonacci(0)
Expecting:
0
ok
Trying:
fibonacci(1)
Expecting:
1
ok
Trying:
fibonacci(10)
Expecting:
55
ok
Trying:
fibonacci(20)
Expecting:
6765
ok
1 items had no tests:
__main__
1 items passed all tests:
4 tests in __main__.fibonacci
4 tests in 2 items.
4 passed and 0 failed.
Test passed.

Maintenant, nous allons changer le code qui a aidé à définir le module Fibonacci
Considérez le bloc de code suivant comme exemple -

def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a

Le bloc de code suivant aide avec les changements -

def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a

Après avoir exécuté le script même sans l'option –v, avec le code modifié, nous obtiendrons la
sortie comme indiqué ci-dessous.

Production
(base) D:\ProgramData>python dock_test.py
**********************************************************************
File "unitg.py", line 6, in __main__.fibonacci
Failed example:
fibonacci(0)
Expected:
0
Got:
1
**********************************************************************
File "unitg.py", line 10, in __main__.fibonacci
Failed example:
fibonacci(10)
Expected:
55
Got:
89
**********************************************************************
File "unitg.py", line 12, in __main__.fibonacci
Failed example:
fibonacci(20)
Expected:
6765
Got:
10946
**********************************************************************
1 items had failures:
3 of 4 in __main__.fibonacci
***Test Failed*** 3 failures.

Nous pouvons voir dans la sortie ci-dessus que trois tests ont échoué.

Débogage des applications de thread


Dans ce chapitre, nous allons apprendre à déboguer des applications de thread. Nous
apprendrons également l'importance du débogage.

Qu'est-ce que le débogage?


Dans la programmation informatique, le débogage est le processus de recherche et de
suppression des bogues, erreurs et anomalies d'un programme informatique. Ce processus
commence dès que le code est écrit et se poursuit par étapes successives lorsque le code est
combiné avec d'autres unités de programmation pour former un produit logiciel. Le débogage fait
partie du processus de test logiciel et fait partie intégrante de l'ensemble du cycle de vie du
développement logiciel.

Débogueur Python
Le débogueur Python ou le pdbfait partie de la bibliothèque standard Python. C'est un bon outil
de secours pour traquer les bogues difficiles à trouver et nous permet de corriger le code
défectueux rapidement et de manière fiable. Les éléments suivants sont les deux tâches les plus
importantes dupdp débogueur -
 Cela nous permet de vérifier les valeurs des variables lors de l'exécution.
 Nous pouvons également parcourir le code et définir des points d'arrêt.
Nous pouvons travailler avec pdb des deux manières suivantes -
 Par la ligne de commande; cela s'appelle également le débogage post-mortem.
 En exécutant pdb de manière interactive.

Travailler avec pdb


Pour travailler avec le débogueur Python, nous devons utiliser le code suivant à l'emplacement
où nous voulons entrer dans le débogueur -

import pdb;
pdb.set_trace()

Considérez les commandes suivantes pour travailler avec pdb via la ligne de commande.
 h(help)
 d(down)
 u(up)
 b(break)
 cl(clear)
 l(list))
 n(next))
 c(continue)
 s(step)
 r(return))
 b(break)
Voici une démonstration de la commande h (help) du débogueur Python -

import pdb

pdb.set_trace()
--Call--
>d:\programdata\lib\site-packages\ipython\core\
displayhook.py(247)__call__()
-> def __call__(self, result = None):
(Pdb) h

Documented commands (type help <topic>):


========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where

Miscellaneous help topics:


==========================
exec pdb

Exemple
Tout en travaillant avec le débogueur Python, nous pouvons définir le point d'arrêt n'importe où
dans le script en utilisant les lignes suivantes -

import pdb;
pdb.set_trace()

Après avoir défini le point d'arrêt, nous pouvons exécuter le script normalement. Le script
s'exécutera jusqu'à un certain point; jusqu'à l'endroit où une ligne a été définie. Prenons
l'exemple suivant où nous exécuterons le script en utilisant les lignes mentionnées ci-dessus à
différents endroits du script -

import pdb;
a = "aaa"
pdb.set_trace()
b = "bbb"
c = "ccc"
final = a + b + c
print (final)

Lorsque le script ci-dessus est exécuté, il exécutera le programme jusqu'à ce que a = «aaa»,
nous pouvons le vérifier dans la sortie suivante.

Production
--Return--
> <ipython-input-7-8a7d1b5cc854>(3)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
*** NameError: name 'b' is not defined
(Pdb) p c
*** NameError: name 'c' is not defined

Après avoir utilisé la commande 'p (print)' dans pdb, ce script n'imprime que 'aaa'. Ceci est suivi
d'une erreur car nous avons défini le point d'arrêt jusqu'à a = "aaa".
De même, nous pouvons exécuter le script en modifiant les points d'arrêt et voir la différence
dans la sortie -

import pdb
a = "aaa"
b = "bbb"
c = "ccc"
pdb.set_trace()
final = a + b + c
print (final)

Production
--Return--
> <ipython-input-9-a59ef5caf723>(5)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
*** NameError: name 'final' is not defined
(Pdb) exit

Dans le script suivant, nous définissons le point d'arrêt dans la dernière ligne du programme -

import pdb
a = "aaa"
b = "bbb"
c = "ccc"
final = a + b + c
pdb.set_trace()
print (final)

La sortie est la suivante -

--Return--
> <ipython-input-11-8019b029997d>(6)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
'aaabbbccc'
(Pdb)

Analyse comparative et profilage


Dans ce chapitre, nous allons découvrir comment l'analyse comparative et le profilage aident à
résoudre les problèmes de performances.
Supposons que nous ayons écrit un code et qu'il donne également le résultat souhaité, mais que
se passe-t-il si nous voulons exécuter ce code un peu plus rapidement car les besoins ont
changé. Dans ce cas, nous devons découvrir quelles parties de notre code ralentissent
l'ensemble du programme. Dans ce cas, l'analyse comparative et le profilage peuvent être utiles.

Qu'est-ce que l'analyse comparative?


Le benchmarking vise à évaluer quelque chose par rapport à une norme. Cependant, la question
qui se pose ici est de savoir quel serait le benchmarking et pourquoi nous en avons besoin en
cas de programmation logicielle. L'analyse comparative du code signifie à quelle vitesse le code
s'exécute et où se trouve le goulot d'étranglement. L'une des principales raisons de l'analyse
comparative est qu'elle optimise le code.

Comment fonctionne l'analyse comparative?


Si nous parlons du fonctionnement de l'analyse comparative, nous devons commencer par
comparer l'ensemble du programme comme un état actuel, puis nous pouvons combiner des
micro-points de référence et décomposer un programme en programmes plus petits. Afin de
trouver les goulots d'étranglement au sein de notre programme et de l'optimiser. En d'autres
termes, nous pouvons le comprendre comme divisant le gros et difficile problème en une série de
problèmes plus petits et un peu plus faciles pour les optimiser.

Module Python pour l'analyse comparative


En Python, nous avons un module par défaut pour l'analyse comparative qui s'appelle timeit.
Avec l'aide dutimeit module, nous pouvons mesurer les performances d'un petit morceau de
code Python dans notre programme principal.

Exemple
Dans le script Python suivant, nous importons le timeit module, qui mesure en outre le temps
nécessaire pour exécuter deux fonctions - functionA et functionB -

import timeit
import time
def functionA():
print("Function A starts the execution:")
print("Function A completes the execution:")
def functionB():
print("Function B starts the execution")
print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)

Après avoir exécuté le script ci-dessus, nous obtiendrons le temps d'exécution des deux
fonctions comme indiqué ci-dessous.

Production
Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076

Écrire notre propre minuterie à l'aide de la


fonction décorateur
En Python, nous pouvons créer notre propre minuterie, qui agira comme le timeitmodule. Cela
peut être fait avec l'aide dudecoratorfonction. Voici un exemple de minuterie personnalisée -

import random
import time

def timer_func(func):

def function_timer(*args, **kwargs):


start = time.time()
value = func(*args, **kwargs)
end = time.time()
runtime = end - start
msg = "{func} took {time} seconds to complete its execution."
print(msg.format(func = func.__name__,time = runtime))
return value
return function_timer

@timer_func
def Myfunction():
for x in range(5):
sleep_time = random.choice(range(1,3))
time.sleep(sleep_time)

if __name__ == '__main__':
Myfunction()

Le script python ci-dessus aide à importer des modules de temps aléatoires. Nous avons créé la
fonction décoratrice timer_func (). Cela a la fonction function_timer () à l'intérieur. Maintenant, la
fonction imbriquée saisira le temps avant d'appeler la fonction passée. Ensuite, il attend le retour
de la fonction et saisit l'heure de fin. De cette façon, nous pouvons enfin faire en sorte que le
script python affiche l'heure d'exécution. Le script générera la sortie comme indiqué ci-dessous.

Production
Myfunction took 8.000457763671875 seconds to complete its execution.

Qu'est-ce que le profilage?


Parfois, le programmeur veut mesurer certains attributs comme l'utilisation de la mémoire, la
complexité du temps ou l'utilisation d'instructions particulières sur les programmes pour mesurer
la capacité réelle de ce programme. Ce type de mesure du programme s'appelle le profilage. Le
profilage utilise une analyse de programme dynamique pour effectuer une telle mesure.
Dans les sections suivantes, nous découvrirons les différents modules Python pour le profilage.

cProfile - le module intégré


cProfileest un module intégré Python pour le profilage. Le module est une extension C avec une
surcharge raisonnable qui le rend approprié pour le profilage de programmes de longue durée.
Après l'avoir exécuté, il enregistre toutes les fonctions et les temps d'exécution. C'est très
puissant mais parfois un peu difficile à interpréter et à appliquer. Dans l'exemple suivant, nous
utilisons cProfile sur le code ci-dessous -

Exemple
def increment_global():

global x
x += 1

def taskofThread(lock):

for _ in range(50000):
lock.acquire()
increment_global()
lock.release()

def main():
global x
x = 0

lock = threading.Lock()

t1 = threading.Thread(target=taskofThread, args=(lock,))
t2 = threading.Thread(target= taskofThread, args=(lock,))

t1.start()
t2.start()

t1.join()
t2.join()

if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))

Le code ci-dessus est enregistré dans le thread_increment.pyfichier. Maintenant, exécutez le


code avec cProfile sur la ligne de commande comme suit -

(base) D:\ProgramData>python -m cProfile thread_increment.py


x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
3577 function calls (3522 primitive calls) in 1.688 seconds

Ordered by: standard name


ncalls tottime percall cumtime percall filename:lineno(function)

5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)


5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
… … … …

De la sortie ci-dessus, il est clair que cProfile imprime toutes les 3577 fonctions appelées, avec le
temps passé dans chacune et le nombre de fois qu'elles ont été appelées. Voici les colonnes que
nous avons reçues en sortie -
 ncalls - C'est le nombre d'appels effectués.
 tottime - C'est le temps total passé dans la fonction donnée.
 percall - Il fait référence au quotient du temps de temps divisé par ncalls.
 cumtime- C'est le temps cumulé passé dans cette fonction et dans toutes les sous-
fonctions. Il est même précis pour les fonctions récursives.
 percall - C'est le quotient cumtime divisé par les appels primitifs.
 filename:lineno(function) - Il fournit essentiellement les données respectives de chaque
fonction.

Concurrence en Python - Pool de threads


Supposons que nous devions créer un grand nombre de threads pour nos tâches multithread. Ce
serait le plus coûteux en calcul car il peut y avoir de nombreux problèmes de performances, en
raison d'un trop grand nombre de threads. Un problème majeur pourrait être la limitation du débit.
Nous pouvons résoudre ce problème en créant un pool de threads. Un pool de threads peut être
défini comme le groupe de threads pré-instanciés et inactifs, qui sont prêts à travailler. La
création d'un pool de threads est préférable à l'instanciation de nouveaux threads pour chaque
tâche lorsque nous devons effectuer un grand nombre de tâches. Un pool de threads peut gérer
l'exécution simultanée d'un grand nombre de threads comme suit -
 Si un thread d'un pool de threads termine son exécution, ce thread peut être réutilisé.
 Si un thread est terminé, un autre thread sera créé pour remplacer ce thread.

Module Python - Concurrent.futures


La bibliothèque standard Python comprend concurrent.futuresmodule. Ce module a été ajouté
dans Python 3.2 pour fournir aux développeurs une interface de haut niveau pour le lancement
de tâches asynchrones. Il s'agit d'une couche d'abstraction au-dessus des modules de thread et
de multiprocessus de Python pour fournir l'interface pour exécuter les tâches à l'aide d'un pool de
threads ou de processus.
Dans nos sections suivantes, nous découvrirons les différentes classes du module
concurrent.futures.

Classe d'exécuteur
Executorest une classe abstraite du concurrent.futuresModule Python. Il ne peut pas être
utilisé directement et nous devons utiliser l'une des sous-classes concrètes suivantes -
 ThreadPoolExecutor
 ProcessPoolExecutor
ThreadPoolExecutor - Une sous-classe concrète
C'est l'une des sous-classes concrètes de la classe Executor. La sous-classe utilise le multi-
threading et nous obtenons un pool de threads pour soumettre les tâches. Ce pool affecte des
tâches aux threads disponibles et planifie leur exécution.

Comment créer un ThreadPoolExecutor?


Avec l'aide de concurrent.futures module et sa sous-classe concrète Executor, nous pouvons
facilement créer un pool de threads. Pour cela, nous devons construire
unThreadPoolExecutoravec le nombre de threads que nous voulons dans le pool. Par défaut, le
nombre est 5. Ensuite, nous pouvons soumettre une tâche au pool de threads. quand
noussubmit() une tâche, nous récupérons un Future. L'objet Future a une méthode
appeléedone(), qui indique si l'avenir est résolu. Avec cela, une valeur a été définie pour cet
objet futur particulier. Lorsqu'une tâche se termine, l'exécuteur du pool de threads définit la valeur
sur l'objet futur.

Exemple
from concurrent.futures import ThreadPoolExecutor
from time import sleep
def task(message):
sleep(2)
return message

def main():
executor = ThreadPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()

Production
False
True
Completed
Dans l'exemple ci-dessus, un ThreadPoolExecutora été construit avec 5 fils. Ensuite, une tâche,
qui attendra 2 secondes avant de donner le message, est soumise à l'exécuteur du pool de
threads. Comme le montre la sortie, la tâche ne se termine pas avant 2 secondes, donc le
premier appel àdone()retournera False. Au bout de 2 secondes, la tâche est terminée et nous
obtenons le résultat du futur en appelant leresult() méthode là-dessus.

Instanciation de ThreadPoolExecutor - Gestionnaire de


contexte
Une autre façon d'instancier ThreadPoolExecutorest avec l'aide du gestionnaire de contexte.
Cela fonctionne de manière similaire à la méthode utilisée dans l'exemple ci-dessus. Le principal
avantage de l'utilisation du gestionnaire de contexte est qu'il a une bonne syntaxe. L'instanciation
peut être effectuée à l'aide du code suivant -

with ThreadPoolExecutor(max_workers = 5) as executor

Exemple
L'exemple suivant est emprunté à la documentation Python. Dans cet exemple, tout d'abord
leconcurrent.futuresle module doit être importé. Puis une fonction nomméeload_url()est créé
qui chargera l'url demandée. La fonction crée alorsThreadPoolExecutoravec les 5 fils dans la
piscine. leThreadPoolExecutora été utilisé comme gestionnaire de contexte. Nous pouvons
obtenir le résultat du futur en appelant leresult() méthode là-dessus.

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']

def load_url(url, timeout):


with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()

with concurrent.futures.ThreadPoolExecutor(max_workers = 5) as executor:

future_to_url = {executor.submit(load_url, url, 60): url for url in


URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))

Production
Voici la sortie du script Python ci-dessus -

'http://some-made-up-domain.com/' generated an exception: <urlopen error


[Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229313 bytes
'http://www.cnn.com/' page is 168933 bytes
'http://www.bbc.co.uk/' page is 283893 bytes
'http://europe.wsj.com/' page is 938109 bytes

Utilisation de la fonction Executor.map ()


Le Python map()La fonction est largement utilisée dans un certain nombre de tâches. L'une de
ces tâches consiste à appliquer une certaine fonction à chaque élément des itérables. De même,
nous pouvons mapper tous les éléments d'un itérateur à une fonction et les soumettre en tant
que jobs indépendants à outThreadPoolExecutor. Prenons l'exemple suivant de script Python
pour comprendre le fonctionnement de la fonction.

Exemple
Dans cet exemple ci-dessous, la fonction de carte est utilisée pour appliquer le square() fonction
à chaque valeur du tableau de valeurs.

from concurrent.futures import ThreadPoolExecutor


from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ThreadPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()

Production
Le script Python ci-dessus génère la sortie suivante -

4
9
16
25

Concurrence en Python - Pool de


processus
Le pool de processus peut être créé et utilisé de la même manière que nous avons créé et utilisé
le pool de threads. Le pool de processus peut être défini comme le groupe de processus pré-
instanciés et inactifs, prêts à recevoir du travail. La création d'un pool de processus est préférable
à l'instanciation de nouveaux processus pour chaque tâche lorsque nous devons effectuer un
grand nombre de tâches.

Module Python - Concurrent.futures


La bibliothèque standard Python a un module appelé le concurrent.futures. Ce module a été
ajouté dans Python 3.2 pour fournir aux développeurs une interface de haut niveau pour le
lancement de tâches asynchrones. Il s'agit d'une couche d'abstraction au-dessus des modules de
thread et de multiprocessus de Python pour fournir l'interface pour exécuter les tâches à l'aide
d'un pool de threads ou de processus.
Dans nos sections suivantes, nous examinerons les différentes sous-classes du module
concurrent.futures.

Classe d'exécuteur
Executor est une classe abstraite du concurrent.futuresModule Python. Il ne peut pas être
utilisé directement et nous devons utiliser l'une des sous-classes concrètes suivantes -
 ThreadPoolExecutor
 ProcessPoolExecutor

ProcessPoolExecutor - Une sous-classe concrète


C'est l'une des sous-classes concrètes de la classe Executor. Il utilise le multi-traitement et nous
obtenons un pool de processus pour soumettre les tâches. Ce pool affecte des tâches aux
processus disponibles et planifie leur exécution.
Comment créer un ProcessPoolExecutor?
Avec l'aide du concurrent.futures module et sa sous-classe concrète Executor, nous pouvons
facilement créer un pool de processus. Pour cela, nous devons construire
unProcessPoolExecutoravec le nombre de processus que nous voulons dans le pool. Par
défaut, le nombre est 5. Ceci est suivi de la soumission d'une tâche au pool de processus.

Exemple
Nous allons maintenant considérer le même exemple que nous avons utilisé lors de la création
du pool de threads, la seule différence étant que nous allons maintenant
utiliser ProcessPoolExecutor au lieu de ThreadPoolExecutor .

from concurrent.futures import ProcessPoolExecutor


from time import sleep
def task(message):
sleep(2)
return message

def main():
executor = ProcessPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()

Production
False
False
Completed

Dans l'exemple ci-dessus, un processusPoolExecutora été construit avec 5 fils. Ensuite, une
tâche, qui attendra 2 secondes avant de donner le message, est soumise à l'exécuteur du pool
de processus. Comme le montre la sortie, la tâche ne se termine pas avant 2 secondes, donc le
premier appel àdone()retournera False. Au bout de 2 secondes, la tâche est terminée et nous
obtenons le résultat du futur en appelant leresult() méthode là-dessus.
Instanciation de ProcessPoolExecutor - Gestionnaire de
contexte
Une autre façon d'instancier ProcessPoolExecutor consiste à utiliser le gestionnaire de contexte.
Cela fonctionne de manière similaire à la méthode utilisée dans l'exemple ci-dessus. Le principal
avantage de l'utilisation du gestionnaire de contexte est qu'il a une bonne syntaxe. L'instanciation
peut être effectuée à l'aide du code suivant -

with ProcessPoolExecutor(max_workers = 5) as executor

Exemple
Pour une meilleure compréhension, nous prenons le même exemple que celui utilisé lors de la
création d'un pool de threads. Dans cet exemple, nous devons commencer par importer
leconcurrent.futuresmodule. Puis une fonction nomméeload_url()est créé qui chargera l'url
demandée. leProcessPoolExecutorest alors créé avec le nombre de 5 threads dans le pool. Le
processusPoolExecutora été utilisé comme gestionnaire de contexte. Nous pouvons obtenir le
résultat du futur en appelant leresult() méthode là-dessus.

import concurrent.futures
from concurrent.futures import ProcessPoolExecutor
import urllib.request

URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']

def load_url(url, timeout):


with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()

def main():
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(load_url, url, 60): url for url in
URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))

if __name__ == '__main__':
main()

Production
Le script Python ci-dessus générera la sortie suivante -

'http://some-made-up-domain.com/' generated an exception: <urlopen error


[Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229476 bytes
'http://www.cnn.com/' page is 165323 bytes
'http://www.bbc.co.uk/' page is 284981 bytes
'http://europe.wsj.com/' page is 967575 bytes

Utilisation de la fonction Executor.map ()


Le Python map()La fonction est largement utilisée pour effectuer un certain nombre de tâches.
L'une de ces tâches consiste à appliquer une certaine fonction à chaque élément des itérables.
De même, nous pouvons mapper tous les éléments d'un itérateur à une fonction et les soumettre
en tant que jobs indépendants auProcessPoolExecutor. Considérez l'exemple suivant de script
Python pour comprendre cela.

Exemple
Nous considérerons le même exemple que nous avons utilisé lors de la création d'un pool de
threads en utilisant le Executor.map()fonction. Dans l'exemple donné ci-dessous, la fonction
map est utilisée pour appliquersquare() fonction à chaque valeur du tableau de valeurs.

from concurrent.futures import ProcessPoolExecutor


from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ProcessPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()

Production
Le script Python ci-dessus générera la sortie suivante

4
9
16
25

Quand utiliser ProcessPoolExecutor et


ThreadPoolExecutor?
Maintenant que nous avons étudié les deux classes Executor - ThreadPoolExecutor et
ProcessPoolExecutor, nous devons savoir quand utiliser quel exécuteur. Nous devons choisir
ProcessPoolExecutor en cas de charges de travail liées au processeur et ThreadPoolExecutor en
cas de charges de travail liées aux E / S.
Si nous utilisons ProcessPoolExecutor, alors nous n'avons pas à nous soucier de GIL car il
utilise le multitraitement. De plus, le temps d'exécution sera moindre par rapport
àThreadPoolExecution. Considérez l'exemple de script Python suivant pour comprendre cela.

Exemple
import time
import concurrent.futures

value = [8000000, 7000000]

def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start

def main():
start = time.time()
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))

if __name__ == '__main__':
main()

Production
Start: 8000000 Time taken: 1.5509998798370361
Start: 7000000 Time taken: 1.3259999752044678
Total time taken: 2.0840001106262207

Example- Python script with ThreadPoolExecutor:


import time
import concurrent.futures

value = [8000000, 7000000]

def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start

def main():
start = time.time()
with concurrent.futures.ThreadPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))

if __name__ == '__main__':
main()

Production
Start: 8000000 Time taken: 3.8420000076293945
Start: 7000000 Time taken: 3.6010000705718994
Total time taken: 3.8480000495910645

À partir des sorties des deux programmes ci-dessus, nous pouvons voir la différence de temps
d'exécution lors de l'utilisation ProcessPoolExecutor et ThreadPoolExecutor.

Concurrence en Python - Multitraitement


Dans ce chapitre, nous nous concentrerons davantage sur la comparaison entre le
multitraitement et le multithreading.

Multitraitement
Il s'agit de l'utilisation de deux unités de processeur ou plus dans un même système informatique.
C'est la meilleure approche pour tirer le plein potentiel de notre matériel en utilisant le nombre
total de cœurs de processeur disponibles dans notre système informatique.

Multithreading
C'est la capacité d'un processeur à gérer l'utilisation du système d'exploitation en exécutant
plusieurs threads simultanément. L'idée principale du multithreading est de réaliser le
parallélisme en divisant un processus en plusieurs threads.
Le tableau suivant montre certaines des différences importantes entre eux -

Multitraitement Multiprogrammation

Le multitraitement fait référence au traitement de plusieurs La multiprogrammation conserve plusieurs progra


processus en même temps par plusieurs processeurs. même temps et les exécute simultanément en uti

Il utilise plusieurs processeurs. Il utilise un seul processeur.

Il permet un traitement parallèle. Le changement de contexte a lieu.

Moins de temps pour traiter les travaux. Plus de temps pour traiter les travaux.

Il facilite une utilisation beaucoup plus efficace des appareils du Moins efficace que le multitraitement.
système informatique.

Habituellement plus cher. De tels systèmes sont moins chers.

Élimination de l'impact du verrouillage


global des interprètes (GIL)
Lorsque vous travaillez avec des applications simultanées, il existe une limitation présente dans
Python appelée GIL (Global Interpreter Lock). GIL ne nous permet jamais d'utiliser plusieurs
cœurs de CPU et nous pouvons donc dire qu'il n'y a pas de vrais threads en Python. GIL est le
mutex - verrou d'exclusion mutuelle, qui sécurise les threads. En d'autres termes, nous pouvons
dire que GIL empêche plusieurs threads d'exécuter du code Python en parallèle. Le verrou ne
peut être détenu que par un seul thread à la fois et si nous voulons exécuter un thread, il doit
d'abord acquérir le verrou.
Avec l'utilisation du multitraitement, nous pouvons efficacement contourner la limitation causée
par GIL -
 En utilisant le multitraitement, nous utilisons la capacité de plusieurs processus et, par
conséquent, nous utilisons plusieurs instances du GIL.
 Pour cette raison, il n'y a aucune restriction d'exécution du bytecode d'un thread dans nos
programmes à tout moment.

Démarrage des processus en Python


Les trois méthodes suivantes peuvent être utilisées pour démarrer un processus en Python dans
le module multitraitement -
 Fork
 Spawn
 Forkserver

Créer un processus avec Fork


La commande Fork est une commande standard trouvée sous UNIX. Il est utilisé pour créer de
nouveaux processus appelés processus enfants. Ce processus enfant s'exécute en même temps
que le processus appelé processus parent. Ces processus enfants sont également identiques à
leurs processus parents et héritent de toutes les ressources disponibles pour le parent. Les
appels système suivants sont utilisés lors de la création d'un processus avec Fork -
 fork()- C'est un appel système généralement implémenté dans le noyau. Il est utilisé pour
créer une copie du processus.p>
 getpid() - Cet appel système renvoie l'ID de processus (PID) du processus appelant.

Exemple
L'exemple de script Python suivant vous aidera à comprendre comment créer un nouveau
processus enfant et obtenir les PID des processus enfants et parents -

import os
def child():
n = os.fork()

if n > 0:
print("PID of Parent process is : ", os.getpid())

else:
print("PID of Child process is : ", os.getpid())
child()

Production
PID of Parent process is : 25989
PID of Child process is : 25990

Créer un processus avec Spawn


Spawn signifie commencer quelque chose de nouveau. Par conséquent, engendrer un processus
signifie la création d'un nouveau processus par un processus parent. Le processus parent
continue son exécution de manière asynchrone ou attend que le processus enfant termine son
exécution. Suivez ces étapes pour générer un processus -
 Importation du module multitraitement.
 Création du processus d'objet.
 Démarrage de l'activité de processus en appelant start() méthode.
 Attendre que le processus ait terminé son travail et quitter en appelant join() méthode.

Exemple
L'exemple suivant de script Python aide à générer trois processus

import multiprocessing

def spawn_process(i):
print ('This is process: %s' %i)
return

if __name__ == '__main__':
Process_jobs = []
for i in range(3):
p = multiprocessing.Process(target = spawn_process, args = (i,))
Process_jobs.append(p)
p.start()
p.join()

Production
This is process: 0
This is process: 1
This is process: 2

Créer un processus avec Forkserver


Le mécanisme de Forkserver n'est disponible que sur les plates-formes UNIX sélectionnées qui
prennent en charge le passage des descripteurs de fichiers sur les tuyaux Unix. Considérez les
points suivants pour comprendre le fonctionnement du mécanisme Forkserver -
 Un serveur est instancié en utilisant le mécanisme Forkserver pour démarrer un nouveau
processus.
 Le serveur reçoit alors la commande et gère toutes les demandes de création de
nouveaux processus.
 Pour créer un nouveau processus, notre programme python enverra une requête à
Forkserver et il créera un processus pour nous.
 Enfin, nous pouvons utiliser ce nouveau processus créé dans nos programmes.

Processus démon en Python


Python multiprocessingmodule nous permet d'avoir des processus démons via son option
démoniaque. Les processus démons ou les processus qui s'exécutent en arrière-plan suivent un
concept similaire à celui des threads démons. Pour exécuter le processus en arrière-plan, nous
devons définir l'indicateur démoniaque sur true. Le processus démon continuera à s'exécuter tant
que le processus principal est en cours d'exécution et il se terminera après avoir terminé son
exécution ou lorsque le programme principal serait tué.

Exemple
Ici, nous utilisons le même exemple que celui utilisé dans les threads du démon. La seule
différence est le changement de module demultithreading à multiprocessinget définissant
l'indicateur démoniaque sur true. Cependant, il y aurait un changement de sortie comme indiqué
ci-dessous -

import multiprocessing
import time

def nondaemonProcess():
print("starting my Process")
time.sleep(8)
print("ending my Process")
def daemonProcess():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
daemonProcess = multiprocessing.Process(target = daemonProcess)
daemonProcess.daemon = True
nondaemonProcess.daemon = False
daemonProcess.start()
nondaemonProcess.start()

Production
starting my Process
ending my Process

La sortie est différente par rapport à celle générée par les threads de démon, car le processus en
mode sans démon a une sortie. Par conséquent, le processus démoniaque se termine
automatiquement après la fin des programmes principaux pour éviter la persistance des
processus en cours d'exécution.

Terminer les processus en Python


Nous pouvons tuer ou terminer un processus immédiatement en utilisant le terminate()méthode.
Nous utiliserons cette méthode pour terminer le processus fils, qui a été créé à l'aide de la
fonction, immédiatement avant de terminer son exécution.

Exemple
import multiprocessing
import time
def Child_process():
print ('Starting function')
time.sleep(5)
print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")

Production
My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated

La sortie montre que le programme se termine avant l'exécution du processus enfant qui a été
créé à l'aide de la fonction Child_process (). Cela implique que le processus enfant s'est terminé
avec succès.

Identifier le processus actuel en Python


Chaque processus du système d'exploitation possède une identité de processus appelée PID. En
Python, nous pouvons connaître le PID du processus actuel à l'aide de la commande suivante -

import multiprocessing
print(multiprocessing.current_process().pid)

Exemple
L'exemple suivant de script Python aide à trouver le PID du processus principal ainsi que le PID
du processus enfant -

import multiprocessing
import time
def Child_process():
print("PID of Child Process is:
{}".format(multiprocessing.current_process().pid))
print("PID of Main process is:
{}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()
Production
PID of Main process is: 9401
PID of Child Process is: 9402

Utilisation d'un processus dans une sous-


classe
Nous pouvons créer des threads en sous-classant le threading.Threadclasse. De plus, nous
pouvons également créer des processus en sous-classant lesmultiprocessing.Processclasse.
Pour utiliser un processus dans une sous-classe, nous devons considérer les points suivants -
 Nous devons définir une nouvelle sous-classe du Process classe.
 Nous devons remplacer le _init_(self [,args] ) classe.
 Nous devons remplacer le de la run(self [,args] ) méthode pour mettre en œuvre
quoi Process
 Nous devons démarrer le processus en invoquant lestart() méthode.

Exemple
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print ('called run method in process: %s' %self.name)
return
if __name__ == '__main__':
jobs = []
for i in range(5):
P = MyProcess()
jobs.append(P)
P.start()
P.join()

Production
called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5
Module multiprocesseur Python - Classe
de pool
Si nous parlons de parallèle simple processingtâches dans nos applications Python, puis le
module multiprocesseur nous fournit la classe Pool. Les méthodes suivantes dePool la classe
peut être utilisée pour lancer un certain nombre de processus enfants dans notre programme
principal

apply () méthode
Cette méthode est similaire à la.submit()méthode de .ThreadPoolExecutor.Il bloque jusqu'à ce
que le résultat soit prêt.

méthode apply_async ()
Lorsque nous avons besoin d'une exécution parallèle de nos tâches, nous devons utiliser
leapply_async()méthode pour soumettre des tâches au pool. Il s'agit d'une opération
asynchrone qui ne verrouille pas le thread principal tant que tous les processus enfants ne sont
pas exécutés.

map () méthode
Tout comme le apply()méthode, il bloque également jusqu'à ce que le résultat soit prêt. C'est
l'équivalent du intégrémap() fonction qui divise les données itérables en un certain nombre de
blocs et les soumet au pool de processus en tant que tâches distinctes.

map_async (), méthode


C'est une variante du map() méthode comme apply_async() est à la apply()méthode. Il renvoie
un objet résultat. Lorsque le résultat est prêt, un appelable lui est appliqué. L'appelable doit être
terminé immédiatement; sinon, le thread qui gère les résultats sera bloqué.

Exemple
L'exemple suivant vous aidera à implémenter un pool de processus pour effectuer une exécution
parallèle. Un simple calcul du carré du nombre a été effectué en appliquant lasquare() fonction à
travers le multiprocessing.Poolméthode. ensuitepool.map() a été utilisé pour soumettre le 5,
car l'entrée est une liste d'entiers de 0 à 4. Le résultat serait stocké dans p_outputs et il est
imprimé.

def square(n):
result = n*n
return result
if __name__ == '__main__':
inputs = list(range(5))
p = multiprocessing.Pool(processes = 4)
p_outputs = pool.map(function_square, inputs)
p.close()
p.join()
print ('Pool :', p_outputs)

Production
Pool : [0, 1, 4, 9, 16]

Processus Intercommunication
L'intercommunication de processus signifie l'échange de données entre les processus. Il est
nécessaire d'échanger les données entre les processus pour le développement d'applications
parallèles. Le diagramme suivant montre les différents mécanismes de communication pour la
synchronisation entre plusieurs sous-processus -

Divers mécanismes de communication


Dans cette section, nous découvrirons les différents mécanismes de communication. Les
mécanismes sont décrits ci-dessous -

Files d'attente
Les files d'attente peuvent être utilisées avec des programmes multi-processus. La classe Queue
demultiprocessing module est similaire au Queue.Queueclasse. Par conséquent, la même API
peut être utilisée.Multiprocessing.Queue nous fournit un mécanisme de communication FIFO
(premier entré premier sorti) sûr pour les threads et les processus.

Exemple
Voici un exemple simple tiré de la documentation officielle de python sur le multitraitement pour
comprendre le concept de classe de file d'attente du multitraitement.

from multiprocessing import Process, Queue


import queue
import random
def f(q):
q.put([42, None, 'hello'])
def main():
q = Queue()
p = Process(target = f, args = (q,))
p.start()
print (q.get())
if __name__ == '__main__':
main()

Production
[42, None, 'hello']

Tuyaux
C'est une structure de données, qui est utilisée pour communiquer entre les processus dans les
programmes multi-processus. La fonction Pipe () renvoie une paire d'objets de connexion
connectés par un tuyau qui par défaut est duplex (bidirectionnel). Cela fonctionne de la manière
suivante -
 Il renvoie une paire d'objets de connexion qui représentent les deux extrémités du tuyau.
 Chaque objet a deux méthodes - send() et recv(), pour communiquer entre les
processus.

Exemple
Voici un exemple simple tiré de la documentation officielle de python sur le multitraitement pour
comprendre le concept de Pipe() fonction du multitraitement.

from multiprocessing import Process, Pipe

def f(conn):
conn.send([42, None, 'hello'])
conn.close()

if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target = f, args = (child_conn,))
p.start()
print (parent_conn.recv())
p.join()

Production
[42, None, 'hello']

Directeur
Manager est une classe de module multitraitement qui fournit un moyen de coordonner les
informations partagées entre tous ses utilisateurs. Un objet gestionnaire contrôle un processus
serveur, qui gère les objets partagés et permet à d'autres processus de les manipuler. En
d'autres termes, les gestionnaires fournissent un moyen de créer des données qui peuvent être
partagées entre différents processus. Voici les différentes propriétés de l'objet gestionnaire -
 La propriété principale du manager est de contrôler un processus serveur, qui gère les
objets partagés.
 Une autre propriété importante est de mettre à jour tous les objets partagés lorsqu'un
processus le modifie.

Exemple
Voici un exemple qui utilise l'objet gestionnaire pour créer un enregistrement de liste dans le
processus serveur, puis ajouter un nouvel enregistrement dans cette liste.

import multiprocessing

def print_records(records):
for record in records:
print("Name: {0}\nScore: {1}\n".format(record[0], record[1]))

def insert_record(record, records):


records.append(record)
print("A New record is added\n")

if __name__ == '__main__':
with multiprocessing.Manager() as manager:
records = manager.list([('Computers', 1), ('Histoty', 5),
('Hindi',9)])
new_record = ('English', 3)

p1 = multiprocessing.Process(target = insert_record, args =


(new_record, records))
p2 = multiprocessing.Process(target = print_records, args =
(records,))
p1.start()
p1.join()
p2.start()
p2.join()

Production
A New record is added

Name: Computers
Score: 1

Name: Histoty
Score: 5

Name: Hindi
Score: 9

Name: English
Score: 3

Concept d'espaces de noms dans Manager


Manager Class est livré avec le concept d'espaces de noms, qui est une méthode rapide pour
partager plusieurs attributs entre plusieurs processus. Les espaces de noms ne comportent
aucune méthode publique, qui peut être appelée, mais ils ont des attributs inscriptibles.

Exemple
L'exemple de script Python suivant nous aide à utiliser les espaces de noms pour partager des
données entre le processus principal et le processus enfant -
import multiprocessing

def Mng_NaSp(using_ns):

using_ns.x +=5
using_ns.y *= 10

if __name__ == '__main__':
manager = multiprocessing.Manager()
using_ns = manager.Namespace()
using_ns.x = 1
using_ns.y = 1

print ('before', using_ns)


p = multiprocessing.Process(target = Mng_NaSp, args = (using_ns,))
p.start()
p.join()
print ('after', using_ns)

Production
before Namespace(x = 1, y = 1)
after Namespace(x = 6, y = 10)

Ctypes-Array et valeur
Le module de multitraitement fournit des objets Array et Value pour stocker les données dans
une carte de mémoire partagée. Array est un tableau ctypes alloué à partir de la mémoire
partagée et Value est un objet ctypes alloué à partir de la mémoire partagée.
Pour être avec, importez Process, Value, Array à partir du multiprocessing.

Exemple
Le script Python suivant est un exemple tiré de la documentation python pour utiliser Ctypes
Array et Value pour partager certaines données entre les processus.

def f(n, a):


n.value = 3.1415927
for i in range(len(a)):
a[i] = -a[i]

if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i', range(10))

p = Process(target = f, args = (num, arr))


p.start()
p.join()
print (num.value)
print (arr[:])

Production
3.1415927
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]

Communication des processus


séquentiels (CSP)
CSP est utilisé pour illustrer l'interaction des systèmes avec d'autres systèmes comportant des
modèles concurrents. CSP est un cadre pour l'écriture simultanée ou d'un programme via le
passage de messages et est donc efficace pour décrire la concurrence.

Bibliothèque Python - PyCSP


Pour implémenter les primitives de base trouvées dans CSP, Python dispose d'une bibliothèque
appelée PyCSP. Il maintient la mise en œuvre très courte et lisible afin qu'elle puisse être
comprise très facilement. Voici le réseau de processus de base de PyCSP -

Dans le réseau de processus PyCSP ci-dessus, il existe deux processus - Process1 et Process
2. Ces processus communiquent en passant des messages via deux canaux - canal 1 et canal 2.
Installation de PyCSP
Avec l'aide de la commande suivante, nous pouvons installer la bibliothèque Python PyCSP -

pip install PyCSP

Exemple
Le script Python suivant est un exemple simple pour exécuter deux processus en parallèle l'un de
l'autre. Cela se fait à l'aide de la bibliothèque Python PyCSP -

from pycsp.parallel import *


import time
@process
def P1():
time.sleep(1)
print('P1 exiting')
@process
def P2():
time.sleep(1)
print('P2 exiting')
def main():
Parallel(P1(), P2())
print('Terminating')
if __name__ == '__main__':
main()

Dans le script ci-dessus, deux fonctions à savoir P1 et P2 ont été créés puis décorés
avec @process pour les convertir en processus.

Production
P2 exiting
P1 exiting
Terminating

Programmation événementielle
La programmation événementielle se concentre sur les événements. Finalement, le déroulement
du programme dépend des événements. Jusqu'à présent, nous avions affaire à un modèle
d'exécution séquentielle ou parallèle, mais le modèle ayant le concept de programmation
événementielle est appelé modèle asynchrone. La programmation événementielle dépend d'une
boucle d'événements qui écoute toujours les nouveaux événements entrants. Le fonctionnement
de la programmation événementielle dépend des événements. Une fois qu'un événement boucle,
les événements décident de ce qu'il faut exécuter et dans quel ordre. L'organigramme suivant
vous aidera à comprendre comment cela fonctionne -

Module Python - Asyncio


Le module Asyncio a été ajouté à Python 3.4 et fournit une infrastructure pour l'écriture de code
simultané à un seul thread à l'aide de co-routines. Voici les différents concepts utilisés par le
module Asyncio -

La boucle d'événements
Event-loop est une fonctionnalité permettant de gérer tous les événements dans un code de
calcul. Il agit en boucle pendant l'exécution du programme entier et garde une trace de l'arrivée et
de l'exécution des événements. Le module Asyncio autorise une seule boucle d'événements par
processus. Voici quelques méthodes fournies par le module Asyncio pour gérer une boucle
d'événements -
 loop = get_event_loop() - Cette méthode fournira la boucle d'événements pour le
contexte actuel.
 loop.call_later(time_delay,callback,argument) - Cette méthode organise le rappel qui
doit être appelé après les secondes time_delay données.
 loop.call_soon(callback,argument)- Cette méthode organise un rappel qui doit être
appelé dès que possible. Le rappel est appelé après le retour de call_soon () et lorsque le
contrôle revient à la boucle d'événements.
 loop.time() - Cette méthode permet de renvoyer l'heure actuelle en fonction de l'horloge
interne de la boucle d'événements.
 asyncio.set_event_loop() - Cette méthode définira la boucle d'événements du contexte
actuel sur la boucle.
 asyncio.new_event_loop() - Cette méthode créera et retournera un nouvel objet de
boucle d'événement.
 loop.run_forever() - Cette méthode fonctionnera jusqu'à ce que la méthode stop () soit
appelée.

Exemple
L'exemple suivant de boucle d'événement aide à l'impression hello worlden utilisant la méthode
get_event_loop (). Cet exemple est tiré de la documentation officielle de Python.

import asyncio

def hello_world(loop):
print('Hello World')
loop.stop()

loop = asyncio.get_event_loop()

loop.call_soon(hello_world, loop)

loop.run_forever()
loop.close()

Production
Hello World

Futures
Ceci est compatible avec la classe concurrent.futures.Future qui représente un calcul qui n'a pas
été effectué. Il existe les différences suivantes entre asyncio.futures.Future et
concurrent.futures.Future -
 Les méthodes result () et exception () ne prennent pas d'argument timeout et lèvent une
exception lorsque le futur n'est pas encore terminé.
 Les rappels enregistrés avec add_done_callback () sont toujours appelés via la fonction
call_soon () de la boucle d'événements.
 La classe asyncio.futures.Future n'est pas compatible avec les fonctions wait () et
as_completed () du package concurrent.futures.

Exemple
Voici un exemple qui vous aidera à comprendre comment utiliser la classe asyncio.futures.future.
import asyncio

async def Myoperation(future):


await asyncio.sleep(2)
future.set_result('Future Completed')

loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(Myoperation(future))
try:
loop.run_until_complete(future)
print(future.result())
finally:
loop.close()

Production
Future Completed

Coroutines
Le concept de coroutines dans Asyncio est similaire au concept d'objet Thread standard sous le
module de threading. C'est la généralisation du concept de sous-programme. Une coroutine peut
être suspendue pendant l'exécution pour qu'elle attende le traitement externe et retourne à partir
du point où elle s'était arrêtée lorsque le traitement externe a été effectué. Les deux méthodes
suivantes nous aident à implémenter des coroutines -

fonction def async ()


Il s'agit d'une méthode d'implémentation de coroutines sous le module Asyncio. Voici un script
Python pour le même -

import asyncio

async def Myoperation():


print("First Coroutine")

loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())

finally:
loop.close()

Production
First Coroutine

@ asyncio.coroutine décorateur
Une autre méthode d'implémentation des coroutines consiste à utiliser des générateurs avec le
décorateur @ asyncio.coroutine. Voici un script Python pour le même -

import asyncio

@asyncio.coroutine
def Myoperation():
print("First Coroutine")

loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())

finally:
loop.close()

Production
First Coroutine

Tâches
Cette sous-classe du module Asyncio est responsable de l'exécution des coroutines dans une
boucle d'événements de manière parallèle. Le script Python suivant est un exemple de traitement
de certaines tâches en parallèle.

import asyncio
import time
async def Task_ex(n):
time.sleep(1)
print("Processing {}".format(n))
async def Generator_task():
for i in range(10):
asyncio.ensure_future(Task_ex(i))
int("Tasks Completed")
asyncio.sleep(2)

loop = asyncio.get_event_loop()
loop.run_until_complete(Generator_task())
loop.close()

Production
Tasks Completed
Processing 0
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Processing 6
Processing 7
Processing 8
Processing 9

Les transports
Le module Asyncio fournit des classes de transport pour implémenter différents types de
communication. Ces classes ne sont pas thread-safe et toujours associées à une instance de
protocole après l'établissement du canal de communication.
Voici différents types de transports hérités de BaseTransport -
 ReadTransport - Il s'agit d'une interface pour les transports en lecture seule.
 WriteTransport - Il s'agit d'une interface pour les transports en écriture seule.
 DatagramTransport - Ceci est une interface pour l'envoi des données.
 BaseSubprocessTransport - Similaire à la classe BaseTransport.
Les éléments suivants sont cinq méthodes distinctes de la classe BaseTransport qui sont par la
suite transitoires sur les quatre types de transport -
 close() - Il ferme le transport.
 is_closing() - Cette méthode retournera true si le transport se ferme ou est déjà fermé.
 get_extra_info(name, default = none) - Cela nous donnera des informations
supplémentaires sur le transport.
 get_protocol() - Cette méthode retournera le protocole actuel.

Protocoles
Le module Asyncio fournit des classes de base que vous pouvez sous-classer pour implémenter
vos protocoles réseau. Ces classes sont utilisées en conjonction avec les transports; le protocole
analyse les données entrantes et demande l'écriture des données sortantes, tandis que le
transport est responsable des E / S et de la mise en mémoire tampon. Voici trois classes de
protocole -
 Protocol - Il s'agit de la classe de base pour l'implémentation de protocoles de streaming
à utiliser avec les transports TCP et SSL.
 DatagramProtocol - Il s'agit de la classe de base pour l'implémentation des protocoles
de datagramme à utiliser avec les transports UDP.
 SubprocessProtocol - Il s'agit de la classe de base pour l'implémentation de protocoles
communiquant avec des processus enfants via un ensemble de canaux unidirectionnels.

Programmation réactive
La programmation réactive est un paradigme de programmation qui traite des flux de données et
de la propagation du changement. Cela signifie que lorsqu'un flux de données est émis par un
composant, le changement sera propagé à d'autres composants par la bibliothèque de
programmation réactive. La propagation du changement continuera jusqu'à ce qu'il atteigne le
récepteur final. La différence entre la programmation événementielle et la programmation
réactive est que la programmation événementielle tourne autour d'événements et la
programmation réactive tourne autour des données.

ReactiveX ou RX pour la programmation


réactive
ReactiveX ou Raective Extension est l'implémentation la plus connue de la programmation
réactive. Le fonctionnement de ReactiveX dépend des deux classes suivantes -

Classe observable
Cette classe est la source du flux de données ou des événements et elle emballe les données
entrantes afin que les données puissent être transmises d'un thread à un autre. Il ne donnera pas
de données tant qu'un observateur ne s'y sera pas abonné.

Classe d'observateur
Cette classe consomme le flux de données émis par observable. Il peut y avoir plusieurs
observateurs avec observable et chaque observateur recevra chaque élément de données émis.
L'observateur peut recevoir trois types d'événements en souscrivant à observable -
 on_next() event - Cela implique qu'il y a un élément dans le flux de données.
 on_completed() event - Cela implique la fin de l'émission et il n'y a plus d'articles à venir.
 on_error() event - Cela implique également la fin de l'émission mais dans le cas où une
erreur est lancée par observable.

RxPY - Module Python pour la


programmation réactive
RxPY est un module Python qui peut être utilisé pour la programmation réactive. Nous devons
nous assurer que le module est installé. La commande suivante peut être utilisée pour installer le
module RxPY -

pip install RxPY

Exemple
Voici un script Python, qui utilise RxPY module et ses classes Observable et Observe
forprogrammation réactive. Il existe essentiellement deux classes -
 get_strings() - pour obtenir les chaînes de l'observateur.
 PrintObserver()- pour imprimer les chaînes de l'observateur. Il utilise les trois
événements de la classe observateur. Il utilise également la classe subscribe ().

from rx import Observable, Observer


def get_strings(observer):
observer.on_next("Ram")
observer.on_next("Mohan")
observer.on_next("Shyam")
observer.on_completed()
class PrintObserver(Observer):
def on_next(self, value):
print("Received {0}".format(value))
def on_completed(self):
print("Finished")
def on_error(self, error):
print("Error: {0}".format(error))
source = Observable.create(get_strings)
source.subscribe(PrintObserver())

Production
Received Ram
Received Mohan
Received Shyam
Finished

Bibliothèque PyFunctional pour la


programmation réactive
PyFunctionalest une autre bibliothèque Python qui peut être utilisée pour la programmation
réactive. Cela nous permet de créer des programmes fonctionnels en utilisant le langage de
programmation Python. Il est utile car il nous permet de créer des pipelines de données en
utilisant des opérateurs fonctionnels chaînés.

Différence entre RxPY et PyFunctional


Les deux bibliothèques sont utilisées pour la programmation réactive et gèrent le flux de la même
manière, mais la principale différence entre les deux dépend de la gestion des
données. RxPY gère les données et les événements du système tout en PyFunctional se
concentre sur la transformation des données à l'aide de paradigmes de programmation
fonctionnelle.

Installation du module PyFunctional


Nous devons installer ce module avant de l'utiliser. Il peut être installé à l'aide de la commande
pip comme suit -

pip install pyfunctional

Exemple
L'exemple suivant utilise the PyFunctional module et son seqclass qui agit comme l'objet de flux
avec lequel nous pouvons itérer et manipuler. Dans ce programme, il mappe la séquence en
utilisant la fonction lamda qui double chaque valeur, puis filtre la valeur où x est supérieur à 4 et
finalement réduit la séquence en une somme de toutes les valeurs restantes.

from functional import seq

result = seq(1,2,3).map(lambda x: x*2).filter(lambda x: x >


4).reduce(lambda x, y: x + y)

print ("Result: {}".format(result))

Production
Result: 6

Vous aimerez peut-être aussi