Discussioni Intercomunicazione

Nella vita reale, se un team di persone sta lavorando a un'attività comune, dovrebbe esserci comunicazione tra loro per portare a termine correttamente l'attività. La stessa analogia è applicabile anche ai thread. In programmazione, per ridurre il tempo ideale del processore creiamo più thread e assegniamo diversi sotto task ad ogni thread. Quindi, deve esserci una struttura di comunicazione e devono interagire tra loro per completare il lavoro in modo sincronizzato.

Considera i seguenti punti importanti relativi all'intercomunicazione tra thread:

  • No performance gain - Se non siamo in grado di ottenere una comunicazione adeguata tra thread e processi, i guadagni in termini di prestazioni dalla concorrenza e dal parallelismo non sono di alcuna utilità.

  • Accomplish task properly - Senza un meccanismo di intercomunicazione appropriato tra i thread, l'attività assegnata non può essere completata correttamente.

  • More efficient than inter-process communication - La comunicazione tra thread è più efficiente e facile da usare rispetto alla comunicazione tra processi perché tutti i thread all'interno di un processo condividono lo stesso spazio di indirizzi e non hanno bisogno di utilizzare la memoria condivisa.

Strutture dati Python per comunicazioni thread-safe

Il codice multithread si presenta con un problema di passare le informazioni da un thread a un altro thread. Le primitive di comunicazione standard non risolvono questo problema. Quindi, dobbiamo implementare il nostro oggetto composito per condividere oggetti tra i thread per rendere la comunicazione thread-safe. Di seguito sono riportate alcune strutture di dati, che forniscono comunicazioni thread-safe dopo aver apportato alcune modifiche in esse:

Imposta

Per utilizzare la struttura dei dati set in modo thread-safe, è necessario estendere la classe set per implementare il nostro meccanismo di blocco.

Esempio

Ecco un esempio Python di estensione della 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()

Nell'esempio precedente, un oggetto classe denominato extend_class è stato definito che è ulteriormente ereditato da Python set class. Un oggetto lock viene creato all'interno del costruttore di questa classe. Ora ci sono due funzioni:add() e delete(). Queste funzioni sono definite e sono thread-safe. Entrambi fanno affidamento sulsuper funzionalità di classe con una chiave di eccezione.

Decoratore

Questo è un altro metodo chiave per la comunicazione thread-safe è l'uso di decoratori.

Esempio

Considera un esempio Python che mostra come usare i decoratori & 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)

Nell'esempio precedente, è stato definito un metodo decorator denominato lock_decorator che è ulteriormente ereditato dalla classe del metodo Python. Quindi un oggetto lock viene creato all'interno del costruttore di questa classe. Ora ci sono due funzioni: add () e delete (). Queste funzioni sono definite e sono thread-safe. Entrambi si basano su funzionalità di classe superiore con un'eccezione fondamentale.

Liste

La struttura dei dati dell'elenco è thread-safe, veloce e semplice per l'archiviazione temporanea in memoria. In Cpython, il GIL protegge dall'accesso simultaneo ad essi. Come siamo venuti a sapere che gli elenchi sono thread-safe, ma per quanto riguarda i dati in essi contenuti. In realtà, i dati della lista non sono protetti. Per esempio,L.append(x)non è garantito per restituire il risultato atteso se un altro thread sta tentando di fare la stessa cosa. Questo perché, sebbeneappend() è un'operazione atomica e thread-safe ma l'altro thread sta cercando di modificare i dati dell'elenco in modo simultaneo, quindi possiamo vedere gli effetti collaterali delle condizioni di gara sull'output.

Per risolvere questo tipo di problema e modificare in modo sicuro i dati, dobbiamo implementare un meccanismo di blocco adeguato, che garantisce ulteriormente che più thread non possano potenzialmente incorrere in condizioni di competizione. Per implementare un meccanismo di blocco appropriato, possiamo estendere la classe come abbiamo fatto negli esempi precedenti.

Alcune altre operazioni atomiche sulle liste sono le seguenti:

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

Qui -

  • L, L1, L2 sono tutte liste
  • D, D1, D2 sono dict
  • x, y sono oggetti
  • io, j sono int

Code

Se i dati dell'elenco non sono protetti, potremmo dover affrontare le conseguenze. Potremmo ottenere o eliminare elementi di dati errati, delle condizioni di gara. Ecco perché si consiglia di utilizzare la struttura dei dati della coda. Un esempio reale di coda può essere una strada a senso unico a una corsia, dove il veicolo entra per primo, esce per primo. Altri esempi dal mondo reale possono essere visti delle code alle biglietterie e alle fermate degli autobus.

Le code sono per impostazione predefinita una struttura di dati thread-safe e non dobbiamo preoccuparci di implementare meccanismi di blocco complessi. Python ci fornisce il file modulo per utilizzare diversi tipi di code nella nostra applicazione.

Tipi di code

In questa sezione, guadagneremo sui diversi tipi di code. Python fornisce tre opzioni di code da usare da<queue> modulo -

  • Code normali (FIFO, First in First out)
  • LIFO, Last in First Out
  • Priority

Impareremo a conoscere le diverse code nelle sezioni successive.

Code normali (FIFO, First in First out)

Sono le implementazioni di code più comunemente usate offerte da Python. In questo meccanismo di accodamento, chiunque arriverà per primo, riceverà per primo il servizio. FIFO è anche chiamato code normali. Le code FIFO possono essere rappresentate come segue:

Implementazione in Python della coda FIFO

In python, la coda FIFO può essere implementata con thread singolo e multithread.

Coda FIFO con thread singolo

Per implementare la coda FIFO con thread singolo, il Queueclass implementerà un contenitore first-in, first-out di base. Gli elementi verranno aggiunti a una "fine" della sequenza utilizzandoput()e rimosso dall'altra estremità utilizzando get().

Esempio

Di seguito è riportato un programma Python per l'implementazione della coda FIFO con thread singolo -

import queue

q = queue.Queue()

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

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

Produzione

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

L'output mostra che il programma precedente utilizza un singolo thread per illustrare che gli elementi vengono rimossi dalla coda nello stesso ordine in cui sono inseriti.

Coda FIFO con più thread

Per implementare FIFO con più thread, dobbiamo definire la funzione myqueue (), che è estesa dal modulo della coda. Il funzionamento dei metodi get () e put () è lo stesso discusso sopra durante l'implementazione della coda FIFO con thread singolo. Quindi per renderlo multithread, dobbiamo dichiarare e istanziare i thread. Questi thread consumeranno la coda in modo FIFO.

Esempio

Di seguito è riportato un programma Python per l'implementazione della coda FIFO con più thread

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

Produzione

<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, ultimo in coda per primo fuori

Questa coda utilizza un'analogia totalmente opposta rispetto alle code FIFO (First in First Out). In questo meccanismo di accodamento, chi arriva per ultimo riceverà il servizio per primo. Questo è simile all'implementazione della struttura dei dati dello stack. Le code LIFO si dimostrano utili durante l'implementazione della ricerca in profondità come algoritmi di intelligenza artificiale.

Implementazione in Python della coda LIFO

In python, la coda LIFO può essere implementata con thread singolo e multithread.

Coda LIFO con thread singolo

Per implementare la coda LIFO con thread singolo, il Queue class implementerà un contenitore di base last-in, first-out utilizzando la struttura Queue.LifoQueue. Ora, sulla chiamataput(), gli elementi vengono aggiunti nella testa del contenitore e rimossi dalla testa anche durante l'utilizzo get().

Esempio

Di seguito è riportato un programma Python per l'implementazione della coda LIFO con thread singolo -

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

L'output mostra che il programma precedente utilizza un singolo thread per illustrare che gli elementi vengono rimossi dalla coda nell'ordine opposto in cui vengono inseriti.

Coda LIFO con più thread

L'implementazione è simile poiché abbiamo implementato code FIFO con più thread. L'unica differenza è che dobbiamo usare l'estensioneQueue classe che implementerà un contenitore last-in, first-out di base utilizzando la struttura Queue.LifoQueue.

Esempio

Di seguito è riportato un programma Python per l'implementazione della coda LIFO con più thread -

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

Produzione

<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

Coda prioritaria

Nelle code FIFO e LIFO, l'ordine degli articoli è correlato all'ordine di inserimento. Tuttavia, ci sono molti casi in cui la priorità è più importante dell'ordine di inserimento. Consideriamo un esempio del mondo reale. Supponiamo che la sicurezza all'aeroporto stia controllando persone di diverse categorie. Le persone del VVIP, il personale della compagnia aerea, l'ufficiale doganale, le categorie possono essere controllate in base alla priorità invece di essere controllate in base all'arrivo come avviene per i cittadini comuni.

Un altro aspetto importante che deve essere considerato per la coda di priorità è come sviluppare un pianificatore di attività. Un progetto comune è quello di servire la maggior parte delle attività dell'agente in base alla priorità nella coda. Questa struttura dati può essere utilizzata per prelevare gli articoli dalla coda in base al loro valore di priorità.

Implementazione in Python della coda prioritaria

In python, la coda di priorità può essere implementata con thread singolo e multithread.

Coda prioritaria con thread singolo

Per implementare la coda di priorità con thread singolo, il Queue class implementerà un'attività sul contenitore prioritario utilizzando la struttura Queue.PriorityQueue. Ora, sulla chiamataput(), gli elementi vengono aggiunti con un valore in cui il valore più basso avrà la priorità più alta e quindi recuperato per primo utilizzando get().

Esempio

Considera il seguente programma Python per l'implementazione della coda Priority con thread singolo:

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)

Produzione

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

Nell'output sopra, possiamo vedere che la coda ha memorizzato gli elementi in base alla priorità: meno valore ha priorità alta.

Coda prioritaria con più thread

L'implementazione è simile all'implementazione di code FIFO e LIFO con più thread. L'unica differenza è che dobbiamo usare l'estensioneQueue class per inizializzare la priorità utilizzando la struttura Queue.PriorityQueue. Un'altra differenza è nel modo in cui verrebbe generata la coda. Nell'esempio riportato di seguito, verrà generato con due set di dati identici.

Esempio

Il seguente programma Python aiuta nell'implementazione della coda di priorità con più thread:

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

Produzione

<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