Programmation parallèle
Utilisation d’un fil d’exécution
1. Gestion d’un fil d’exécution
a. Présentation
Python a longtemps proposé un module nommé thread qui proposait une API de bas niveau et qui permettait de créer des fils d’exécution selon différentes solutions qui pouvaient correspondre aux capacités de Python en ce domaine.
L’implémentation CPython ayant des pans entiers de son code incompatibles avec une création propre de fils d’exécution, ce module a évolué et a finalement été déprécié avec la finalisation d’une API de haut niveau threading qui est à la fois plus simple à utiliser et plus proche des besoins, mais aussi plus fiable.
Pour Python 3.x, l’ancien module de bas niveau déprécié a été simplement retiré (renommé avec un souligné (http://www.python.org/dev/peps/pep-3108/), voir la PEP au chapitre Obsolete) et le nouveau module bénéficie de tous les efforts portés sur l’implémentation de CPython pour permettre d’avoir de vrais fils d’exécution.
Comme on le verra, la problématique n’est pas tellement la création et l’exécution en parallèle de fils d’exécution, mais plutôt la gestion des ressources qu’ils partagent et de leurs communications.
b. Création
Pour créer un fil d’exécution, il faut utiliser le module de haut niveau :
>>> from threading import Thread
Pour les besoins de cet exemple, nous chargeons également ceci :
>>> from time import time, ctime, sleep
Voici la déclaration d’un fil d’exécution, suivie de son utilisation :
>>> class Worker(Thread):
... def __init__(self, name, delay):
... self.delay = delay
... Thread.__init__(self, name=name)
... def run(self):
... for i in range(5):
... print("%s: Appel %s, %s" % (self.getName(), i, ctime(time())))
... sleep(self.delay)
...
>>> try:
... t1 = Worker("T1"...
Utilisation de processus
1. Gestion d’un processus
a. Présentation
Sur un certain nombre de plans, vu de l’interface de haut niveau fournie par Python, le multitâche (ou multi-fils) est semblable au multiprocessus. Mais la ressemblance s’arrête à celle des listes de méthodes proposées par les objets.
Les problématiques à résoudre sont plus complexes. Leur avantage est de permettre d’exécuter deux actions au même moment sur deux processeurs différents et donc diminuer leur temps d’exécution.
Un processus est indépendant, embarque son propre environnement d’exécution. Il est également lié au processus qui le crée, qui peut être la console Python, la machine virtuelle Python ou un autre processus Python.
b. Création
Créer un processus est relativement simple. Voici les modules nécessaires :
>>> from multiprocessing import Process
>>> from time import sleep
Voici un travail à adjoindre au processus :
>>> def work(name):
... print('Debut du travail: %s' % name)
... for j in range(10):
... for i in range(10):
... sleep(0.01)
... print('.', sep='', end='')
... print('.')
... print('Fin du travail: %s' % name)
...
Il suffit maintenant de créer le processus, et de le démarrer :
>>> p = Process(target=work, args=('Test',))
>>> p.start()
>>> p.join()
Voici le résultat :
Debut du travail: Test
...........
...........
...........
...........
...........
...........
...........
...........
...........
...........
Fin du travail: Test
On va mettre en évidence les caractéristiques du processus :
>>> print ('Flux principal: %s' % os.getpid())
Flux principal: 8879
Le processus a pour PID parent ce numéro :
>>> def work(name):
... print('Debut du travail: %s' % name)
... print('Pid: %s, parent:...
Exécution asynchrone
1. Introduction
Jusqu’à présent, on a vu comment créer des fils d’exécution et les gérer avec des outils de haut niveau permettant de donner du travail à chaque fil d’exécution, comment les utiliser au mieux et gérer leur exécution finement.
On a également vu comment créer des processus et les gérer toujours avec des outils de haut niveau.
Ces opérations, par rapport à ce qu’elles impliquent comme technicité pour les langages de bas niveau, sont relativement simples à utiliser et on a vu différents cas d’application et différentes mises en œuvre possibles.
Il reste également possible de réaliser des opérations de plus bas niveau encore, comme l’utilisation du fork qui reste disponible, même si ce présent chapitre n’insiste pas spécialement sur cette fonctionnalité, préférant mettre le focus sur le haut niveau.
Ainsi, ceux qui connaissent le fork en C seront capables d’effectuer la même opération en Python, car il s’agit de la même chose. De plus, les règles de programmation sont communes à tous les langages.
Cependant, l’utilisation du bas niveau, voire des deux modules de haut niveau que sont threading et multiprocessing induit des risques et nécessite une certaine maîtrise, même si cela est beaucoup plus simple qu’avec des langages de plus bas niveau, comme le C.
Le grand risque est de créer un processus ou un fil d’exécution dont on perd la maîtrise. Si une telle chose arrive, le fil d’exécution ou le processus peut très bien occuper à 100 % le processeur et ne pas rendre la main pendant un temps long, potentiellement problématique pour les autres processus tournant sur la machine (au-delà des autres fils d’exécution ou processus Python) et cela peut entraîner le plantage du système d’exploitation.
Heureusement, les outils de haut niveau que l’on a vus jusqu’à présent, s’ils sont bien utilisés, permettent d’éviter cela. Le recours à sleep au sein des fils d’exécution et processus est certes coûteux, mais il est nécessaire...