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.