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