Themen Interkommunikation

Wenn im wirklichen Leben ein Team von Menschen an einer gemeinsamen Aufgabe arbeitet, sollte zwischen ihnen eine Kommunikation bestehen, um die Aufgabe ordnungsgemäß zu erledigen. Die gleiche Analogie gilt auch für Threads. Um die ideale Zeit des Prozessors zu verkürzen, erstellen wir bei der Programmierung mehrere Threads und weisen jedem Thread unterschiedliche Unteraufgaben zu. Daher muss eine Kommunikationseinrichtung vorhanden sein, und sie sollten miteinander interagieren, um den Auftrag synchron zu beenden.

Berücksichtigen Sie die folgenden wichtigen Punkte im Zusammenhang mit der Thread-Interkommunikation:

  • No performance gain - Wenn wir keine ordnungsgemäße Kommunikation zwischen Threads und Prozessen erreichen können, sind die Leistungsgewinne durch Parallelität und Parallelität nutzlos.

  • Accomplish task properly - Ohne einen ordnungsgemäßen Interkommunikationsmechanismus zwischen Threads kann die zugewiesene Aufgabe nicht ordnungsgemäß ausgeführt werden.

  • More efficient than inter-process communication - Die Kommunikation zwischen Threads ist effizienter und benutzerfreundlicher als die Kommunikation zwischen Prozessen, da alle Threads innerhalb eines Prozesses denselben Adressraum verwenden und keinen gemeinsamen Speicher verwenden müssen.

Python-Datenstrukturen für die thread-sichere Kommunikation

Multithread-Code hat das Problem, Informationen von einem Thread an einen anderen Thread zu übergeben. Die Standard-Kommunikationsprimitive lösen dieses Problem nicht. Daher müssen wir unser eigenes zusammengesetztes Objekt implementieren, um Objekte zwischen Threads zu teilen, um die Kommunikation threadsicher zu machen. Im Folgenden finden Sie einige Datenstrukturen, die nach einigen Änderungen eine thread-sichere Kommunikation ermöglichen.

Sets

Um die Set-Datenstruktur threadsicher zu verwenden, müssen wir die Set-Klasse erweitern, um unseren eigenen Sperrmechanismus zu implementieren.

Beispiel

Hier ist ein Python-Beispiel für die Erweiterung der Klasse -

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

Im obigen Beispiel wurde ein Klassenobjekt benannt extend_class wurde definiert, die weiter von Python geerbt wird set class. Im Konstruktor dieser Klasse wird ein Sperrobjekt erstellt. Nun gibt es zwei Funktionen -add() und delete(). Diese Funktionen sind definiert und threadsicher. Sie verlassen sich beide auf diesuper Klassenfunktionalität mit einer Schlüsselausnahme.

Dekorateur

Dies ist eine weitere wichtige Methode für die thread-sichere Kommunikation, bei der Dekoratoren verwendet werden.

Beispiel

Stellen Sie sich ein Python-Beispiel vor, das zeigt, wie Dekoratoren verwendet werden & 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)

Im obigen Beispiel wurde eine Dekorationsmethode mit dem Namen lock_decorator definiert, die weiter von der Python-Methodenklasse geerbt wird. Anschließend wird im Konstruktor dieser Klasse ein Sperrobjekt erstellt. Jetzt gibt es zwei Funktionen - add () und delete (). Diese Funktionen sind definiert und threadsicher. Beide setzen mit einer Ausnahme auf erstklassige Funktionen.

Listen

Die Listendatenstruktur ist threadsicher, schnell und einfach für die temporäre Speicherung im Speicher. In Cpython schützt die GIL vor gleichzeitigem Zugriff auf sie. Als wir erfuhren, dass Listen threadsicher sind, aber was ist mit den darin enthaltenen Daten? Tatsächlich sind die Daten der Liste nicht geschützt. Zum Beispiel,L.append(x)Es kann nicht garantiert werden, dass das erwartete Ergebnis zurückgegeben wird, wenn ein anderer Thread versucht, dasselbe zu tun. Dies liegt jedoch daranappend() ist eine atomare Operation und threadsicher, aber der andere Thread versucht, die Daten der Liste gleichzeitig zu ändern, sodass wir die Nebenwirkungen der Race-Bedingungen auf die Ausgabe sehen können.

Um diese Art von Problem zu beheben und die Daten sicher zu ändern, müssen wir einen geeigneten Sperrmechanismus implementieren, der ferner sicherstellt, dass mehrere Threads möglicherweise nicht unter Rennbedingungen laufen können. Um einen ordnungsgemäßen Sperrmechanismus zu implementieren, können wir die Klasse wie in den vorherigen Beispielen erweitern.

Einige andere atomare Operationen auf Listen sind wie folgt:

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

Hier -

  • L, L1, L2 sind alle Listen
  • D, D1, D2 sind Diktate
  • x, y sind Objekte
  • Ich, ich bin Ints

Warteschlangen

Wenn die Daten der Liste nicht geschützt sind, müssen wir uns möglicherweise den Konsequenzen stellen. Wir können falsche Daten der Rennbedingungen erhalten oder löschen. Aus diesem Grund wird empfohlen, die Warteschlangendatenstruktur zu verwenden. Ein reales Beispiel für eine Warteschlange kann eine einspurige Einbahnstraße sein, bei der das Fahrzeug zuerst ein- und zuerst ausfährt. Weitere Beispiele aus der Praxis sind die Warteschlangen an den Fahrkartenschaltern und Bushaltestellen.

Warteschlangen sind standardmäßig threadsichere Datenstrukturen, und wir müssen uns nicht um die Implementierung komplexer Sperrmechanismen kümmern. Python bietet uns die Modul zur Verwendung verschiedener Arten von Warteschlangen in unserer Anwendung.

Arten von Warteschlangen

In diesem Abschnitt werden wir uns mit den verschiedenen Arten von Warteschlangen befassen. Python bietet drei Optionen für Warteschlangen, die aus dem verwendet werden können<queue> Modul -

  • Normale Warteschlangen (FIFO, First in First out)
  • LIFO, Last in First Out
  • Priority

In den folgenden Abschnitten erfahren Sie mehr über die verschiedenen Warteschlangen.

Normale Warteschlangen (FIFO, First in First out)

Es sind die am häufigsten verwendeten Warteschlangenimplementierungen, die von Python angeboten werden. In diesem Warteschlangenmechanismus erhält jeder, der zuerst kommt, zuerst den Dienst. FIFO wird auch als normale Warteschlange bezeichnet. FIFO-Warteschlangen können wie folgt dargestellt werden:

Python-Implementierung der FIFO-Warteschlange

In Python kann die FIFO-Warteschlange sowohl mit einem Thread als auch mit Multithreads implementiert werden.

FIFO-Warteschlange mit einem Thread

Für die Implementierung der FIFO-Warteschlange mit einem einzelnen Thread wird die QueueDie Klasse implementiert einen einfachen First-In- und First-Out-Container. Elemente werden an einem „Ende“ der Sequenz mit hinzugefügtput()und vom anderen Ende mit entfernt get().

Beispiel

Es folgt ein Python-Programm zur Implementierung der FIFO-Warteschlange mit einem einzelnen Thread -

import queue

q = queue.Queue()

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

while not q.empty():
   print (q.get(), end = " ")

Ausgabe

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

Die Ausgabe zeigt, dass das obige Programm einen einzelnen Thread verwendet, um zu veranschaulichen, dass die Elemente in derselben Reihenfolge aus der Warteschlange entfernt werden, in der sie eingefügt werden.

FIFO-Warteschlange mit mehreren Threads

Um FIFO mit mehreren Threads zu implementieren, müssen wir die Funktion myqueue () definieren, die vom Warteschlangenmodul erweitert wird. Die Funktionsweise der Methoden get () und put () ist dieselbe wie oben beschrieben, während die FIFO-Warteschlange mit einem einzelnen Thread implementiert wird. Um es dann multithreaded zu machen, müssen wir die Threads deklarieren und instanziieren. Diese Threads belegen die Warteschlange auf FIFO-Weise.

Beispiel

Es folgt ein Python-Programm zur Implementierung einer FIFO-Warteschlange mit mehreren 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()

Ausgabe

<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, Last in First Out-Warteschlange

Diese Warteschlange verwendet eine völlig entgegengesetzte Analogie zu FIFO-Warteschlangen (First in First Out). In diesem Warteschlangenmechanismus erhält derjenige, der zuletzt kommt, zuerst den Dienst. Dies ähnelt der Implementierung einer Stapeldatenstruktur. LIFO-Warteschlangen erweisen sich als nützlich bei der Implementierung der Tiefensuche wie Algorithmen der künstlichen Intelligenz.

Python-Implementierung der LIFO-Warteschlange

In Python kann die LIFO-Warteschlange sowohl mit einem Thread als auch mit Multithreads implementiert werden.

LIFO-Warteschlange mit einem Thread

Für die Implementierung der LIFO-Warteschlange mit einem einzelnen Thread wird die Queue Die Klasse implementiert mithilfe der Struktur einen grundlegenden Last-In- und First-Out-Container Queue.LifoQueue. Nun zum Anrufput()werden die Elemente im Kopf des Behälters hinzugefügt und auch bei Verwendung aus dem Kopf entfernt get().

Beispiel

Es folgt ein Python-Programm zur Implementierung der LIFO-Warteschlange mit einem einzelnen 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

Die Ausgabe zeigt, dass das obige Programm einen einzelnen Thread verwendet, um zu veranschaulichen, dass Elemente in der entgegengesetzten Reihenfolge aus der Warteschlange entfernt werden, in der sie eingefügt werden.

LIFO-Warteschlange mit mehreren Threads

Die Implementierung ist ähnlich wie bei der Implementierung von FIFO-Warteschlangen mit mehreren Threads. Der einzige Unterschied ist, dass wir das verwenden müssenQueue Klasse, die mithilfe der Struktur einen grundlegenden Last-In- und First-Out-Container implementiert Queue.LifoQueue.

Beispiel

Es folgt ein Python-Programm zur Implementierung einer LIFO-Warteschlange mit mehreren 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()

Ausgabe

<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

Prioritätswarteschlange

In FIFO- und LIFO-Warteschlangen hängt die Reihenfolge der Elemente mit der Reihenfolge des Einfügens zusammen. Es gibt jedoch viele Fälle, in denen die Priorität wichtiger ist als die Reihenfolge des Einfügens. Betrachten wir ein Beispiel aus der Praxis. Angenommen, die Sicherheit am Flughafen überprüft Personen verschiedener Kategorien. Personen der VVIP, Mitarbeiter der Fluggesellschaft, Zollbeamte und Kategorien können vorrangig überprüft werden, anstatt wie bei den Bürgern auf der Grundlage der Ankunft überprüft zu werden.

Ein weiterer wichtiger Aspekt, der für die Prioritätswarteschlange berücksichtigt werden muss, ist die Entwicklung eines Taskplaners. Ein gängiges Design besteht darin, die meisten Agentenaufgaben vorrangig in der Warteschlange zu erledigen. Diese Datenstruktur kann verwendet werden, um die Elemente basierend auf ihrem Prioritätswert aus der Warteschlange aufzunehmen.

Python-Implementierung der Prioritätswarteschlange

In Python kann die Prioritätswarteschlange sowohl mit einem Thread als auch mit Multithreads implementiert werden.

Prioritätswarteschlange mit einem Thread

Für die Implementierung der Prioritätswarteschlange mit einem einzelnen Thread wird die Queue Die Klasse implementiert eine Aufgabe für den Prioritätscontainer mithilfe der Struktur Queue.Prioritätswarteschlange. Nun zum Anrufput()werden die Elemente mit einem Wert hinzugefügt, bei dem der niedrigste Wert die höchste Priorität hat, und daher zuerst mithilfe von abgerufen get().

Beispiel

Betrachten Sie das folgende Python-Programm für die Implementierung der Prioritätswarteschlange mit einem einzelnen 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)

Ausgabe

1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important

In der obigen Ausgabe können wir sehen, dass die Warteschlange die Elemente basierend auf der Priorität gespeichert hat - weniger Wert hat hohe Priorität.

Prioritätswarteschlange mit mehreren Threads

Die Implementierung ähnelt der Implementierung von FIFO- und LIFO-Warteschlangen mit mehreren Threads. Der einzige Unterschied ist, dass wir das verwenden müssenQueue Klasse zum Initialisieren der Priorität mithilfe der Struktur Queue.PriorityQueue. Ein weiterer Unterschied besteht in der Art und Weise, wie die Warteschlange generiert wird. Im folgenden Beispiel wird es mit zwei identischen Datensätzen generiert.

Beispiel

Das folgende Python-Programm hilft bei der Implementierung der Prioritätswarteschlange mit mehreren 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()

Ausgabe

<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