Threads synchronisieren

Die Thread-Synchronisation kann als eine Methode definiert werden, mit deren Hilfe sichergestellt werden kann, dass zwei oder mehr gleichzeitige Threads nicht gleichzeitig auf das als kritischer Abschnitt bekannte Programmsegment zugreifen. Andererseits ist bekanntlich dieser kritische Abschnitt der Teil des Programms, auf den auf die gemeinsam genutzte Ressource zugegriffen wird. Daher können wir sagen, dass bei der Synchronisierung sichergestellt wird, dass zwei oder mehr Threads nicht miteinander verbunden sind, indem gleichzeitig auf die Ressourcen zugegriffen wird. Das folgende Diagramm zeigt, dass vier Threads gleichzeitig versuchen, auf den kritischen Abschnitt eines Programms zuzugreifen.

Nehmen wir zur Verdeutlichung an, dass zwei oder mehr Threads gleichzeitig versuchen, das Objekt zur Liste hinzuzufügen. Dieser Vorgang kann nicht zu einem erfolgreichen Ende führen, da entweder ein oder alle Objekte gelöscht werden oder der Status der Liste vollständig beschädigt wird. Hier besteht die Rolle der Synchronisation darin, dass jeweils nur ein Thread auf die Liste zugreifen kann.

Probleme bei der Thread-Synchronisation

Beim Implementieren der gleichzeitigen Programmierung oder beim Anwenden synchronisierender Grundelemente können Probleme auftreten. In diesem Abschnitt werden wir zwei Hauptthemen diskutieren. Die Probleme sind -

  • Deadlock
  • Rennbedingung

Rennbedingung

Dies ist eines der Hauptprobleme bei der gleichzeitigen Programmierung. Der gleichzeitige Zugriff auf gemeinsam genutzte Ressourcen kann zu Rennbedingungen führen. Eine Race-Bedingung kann als das Auftreten einer Bedingung definiert werden, wenn zwei oder mehr Threads auf gemeinsam genutzte Daten zugreifen und dann versuchen können, ihren Wert gleichzeitig zu ändern. Aus diesem Grund können die Werte von Variablen unvorhersehbar sein und abhängig von den Zeitpunkten der Kontextwechsel der Prozesse variieren.

Beispiel

Betrachten Sie dieses Beispiel, um das Konzept der Rennbedingungen zu verstehen -

Step 1 - In diesem Schritt müssen wir das Threading-Modul importieren -

import threading

Step 2 - Definieren Sie nun eine globale Variable, z. B. x, zusammen mit ihrem Wert als 0 -

x = 0

Step 3 - Jetzt müssen wir das definieren increment_global() Funktion, die in dieser globalen Funktion das Inkrement um 1 ausführt x -

def increment_global():

   global x
   x += 1

Step 4 - In diesem Schritt definieren wir die taskofThread()Funktion, die die Funktion increment_global () für eine bestimmte Anzahl von Malen aufruft; für unser Beispiel ist es 50000 mal -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- Definieren Sie nun die main () - Funktion, in der die Threads t1 und t2 erstellt werden. Beide werden mit Hilfe der Funktion start () gestartet und warten, bis sie ihre Arbeit mit Hilfe der Funktion join () beendet haben.

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- Nun müssen wir den Bereich angeben, für wie viele Iterationen wir die main () - Funktion aufrufen möchten. Hier rufen wir es 5 mal an.

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

In der unten gezeigten Ausgabe können wir den Effekt der Rennbedingung als den Wert von x sehen, nachdem jede Iteration 100000 erwartet wird. Es gibt jedoch viele Variationen im Wert. Dies ist auf den gleichzeitigen Zugriff von Threads auf die gemeinsam genutzte globale Variable x zurückzuführen.

Ausgabe

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

Umgang mit Rennbedingungen mit Schlössern

Da wir die Auswirkung der Race-Bedingung im obigen Programm gesehen haben, benötigen wir ein Synchronisationstool, das die Race-Bedingung zwischen mehreren Threads verarbeiten kann. In Python ist die<threading>Das Modul bietet eine Lock-Klasse für den Umgang mit Rennbedingungen. Weiterhin ist dieLockDie Klasse bietet verschiedene Methoden, mit deren Hilfe wir die Race-Bedingungen zwischen mehreren Threads behandeln können. Die Methoden werden nachfolgend beschrieben -

erwerben () Methode

Diese Methode wird verwendet, um eine Sperre zu erfassen, dh zu blockieren. Eine Sperre kann abhängig vom folgenden wahren oder falschen Wert blockieren oder nicht blockieren -

  • With value set to True - Wenn die Methode purchase () mit True aufgerufen wird, was das Standardargument ist, wird die Thread-Ausführung blockiert, bis die Sperre aufgehoben wird.

  • With value set to False - Wenn die Methode purchase () mit False aufgerufen wird, was nicht das Standardargument ist, wird die Thread-Ausführung erst blockiert, wenn sie auf true gesetzt ist, dh bis sie gesperrt ist.

release () Methode

Diese Methode wird verwendet, um eine Sperre aufzuheben. Im Folgenden sind einige wichtige Aufgaben im Zusammenhang mit dieser Methode aufgeführt:

  • Wenn ein Schloss gesperrt ist, wird das release()Methode würde es entsperren. Seine Aufgabe ist es, genau einem Thread zu erlauben, fortzufahren, wenn mehr als ein Thread blockiert ist und darauf wartet, dass die Sperre entsperrt wird.

  • Es wird ein erhöhen ThreadError wenn die Sperre bereits entsperrt ist.

Jetzt können wir das obige Programm mit der Lock-Klasse und ihren Methoden umschreiben, um die Race-Bedingung zu vermeiden. Wir müssen die taskofThread () -Methode mit dem Argument lock definieren und dann die Methoden purchase () und release () zum Blockieren und Nichtblockieren von Sperren verwenden, um Race-Bedingungen zu vermeiden.

Beispiel

Das folgende Beispiel zeigt ein Python-Programm, um das Konzept von Sperren für den Umgang mit Race-Bedingungen zu verstehen:

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))

Die folgende Ausgabe zeigt, dass der Effekt der Rennbedingung vernachlässigt wird. Der Wert von x beträgt nach jeder Iteration jetzt 100000, was den Erwartungen dieses Programms entspricht.

Ausgabe

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 - Das Problem der Dining Philosophen

Deadlock ist ein problematisches Problem beim Entwerfen der gleichzeitigen Systeme. Wir können dieses Problem mit Hilfe des Essensphilosophenproblems wie folgt veranschaulichen:

Edsger Dijkstra führte ursprünglich das Problem des Speisephilosophen ein, eines der bekanntesten Beispiele für eines der größten Probleme des gleichzeitigen Systems namens Deadlock.

In diesem Problem sitzen fünf berühmte Philosophen an einem runden Tisch und essen etwas aus ihren Schalen. Es gibt fünf Gabeln, mit denen die fünf Philosophen ihr Essen essen können. Die Philosophen beschließen jedoch, zwei Gabeln gleichzeitig zu verwenden, um ihr Essen zu essen.

Nun gibt es zwei Hauptbedingungen für die Philosophen. Erstens kann jeder der Philosophen entweder essen oder denken, und zweitens müssen sie zuerst beide Gabeln erhalten, dh links und rechts. Das Problem tritt auf, wenn jeder der fünf Philosophen gleichzeitig die linke Gabel auswählt. Jetzt warten alle darauf, dass die richtige Gabel frei ist, aber sie werden ihre Gabel niemals abgeben, bis sie ihr Essen gegessen haben und die richtige Gabel niemals verfügbar sein würde. Daher würde es am Esstisch einen Deadlock-Zustand geben.

Deadlock im gleichzeitigen System

Wenn wir jetzt sehen, kann das gleiche Problem auch in unseren gleichzeitigen Systemen auftreten. Die Gabeln im obigen Beispiel wären die Systemressourcen, und jeder Philosoph kann den Prozess darstellen, der im Wettbewerb um die Ressourcen steht.

Lösung mit Python-Programm

Die Lösung dieses Problems kann gefunden werden, indem die Philosophen in zwei Typen aufgeteilt werden - greedy philosophers und generous philosophers. Hauptsächlich wird ein gieriger Philosoph versuchen, die linke Gabel aufzunehmen und zu warten, bis sie da ist. Er wird dann warten, bis die richtige Gabel da ist, sie aufheben, essen und dann ablegen. Auf der anderen Seite wird ein großzügiger Philosoph versuchen, die linke Gabel aufzuheben, und wenn sie nicht da ist, wird er warten und es nach einiger Zeit erneut versuchen. Wenn sie die linke Gabel bekommen, werden sie versuchen, die richtige zu bekommen. Wenn sie auch die richtige Gabel bekommen, essen sie beide Gabeln und lassen sie los. Wenn sie jedoch nicht die richtige Gabel bekommen, lassen sie die linke Gabel los.

Beispiel

Das folgende Python-Programm hilft uns, eine Lösung für das Problem des Essensphilosophen zu finden:

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()

Das obige Programm verwendet das Konzept gieriger und großzügiger Philosophen. Das Programm hat auch die verwendetacquire() und release() Methoden der Lock Klasse der <threading>Modul. Wir können die Lösung in der folgenden Ausgabe sehen -

Ausgabe

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.