Sincronizzazione dei thread

La sincronizzazione dei thread può essere definita come un metodo con l'aiuto del quale possiamo essere certi che due o più thread simultanei non accedono simultaneamente al segmento di programma noto come sezione critica. D'altra parte, come sappiamo, quella sezione critica è la parte del programma in cui si accede alla risorsa condivisa. Quindi possiamo dire che la sincronizzazione è il processo che assicura che due o più thread non si interfacciano tra loro accedendo alle risorse contemporaneamente. Il diagramma seguente mostra che quattro thread tentano di accedere contemporaneamente alla sezione critica di un programma.

Per renderlo più chiaro, supponiamo che due o più thread provino ad aggiungere l'oggetto nell'elenco contemporaneamente. Questo atto non può portare a una conclusione positiva perché o lascerà cadere uno o tutti gli oggetti o corromperà completamente lo stato della lista. Qui il ruolo della sincronizzazione è che solo un thread alla volta può accedere all'elenco.

Problemi nella sincronizzazione dei thread

Potremmo incontrare problemi durante l'implementazione della programmazione concorrente o l'applicazione di primitive di sincronizzazione. In questa sezione, discuteremo di due questioni principali. I problemi sono:

  • Deadlock
  • Condizione di gara

Condizione di gara

Questo è uno dei problemi principali nella programmazione concorrente. L'accesso simultaneo alle risorse condivise può portare a condizioni di competizione. Una condizione di competizione può essere definita come il verificarsi di una condizione quando due o più thread possono accedere ai dati condivisi e quindi provare a modificarne il valore allo stesso tempo. Per questo motivo, i valori delle variabili possono essere imprevedibili e variare a seconda dei tempi dei cambi di contesto dei processi.

Esempio

Considera questo esempio per comprendere il concetto di condizione di gara:

Step 1 - In questo passaggio, dobbiamo importare il modulo di threading -

import threading

Step 2 - Ora, definisci una variabile globale, ad esempio x, insieme al suo valore 0 -

x = 0

Step 3 - Ora, dobbiamo definire il file increment_global() funzione, che farà l'incremento di 1 in questa funzione globale x -

def increment_global():

   global x
   x += 1

Step 4 - In questo passaggio, definiremo il file taskofThread()funzione, che chiamerà la funzione increment_global () per un numero di volte specificato; per il nostro esempio è 50000 volte -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- Ora, definisci la funzione main () in cui vengono creati i thread t1 e t2. Entrambi verranno avviati con l'aiuto della funzione start () e attenderanno fino a quando non finiranno i loro lavori con l'aiuto della funzione 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- Ora, dobbiamo fornire l'intervallo per quante iterazioni vogliamo chiamare la funzione main (). Qui lo chiamiamo per 5 volte.

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

Nell'output mostrato di seguito, possiamo vedere l'effetto della race condition poiché il valore di x dopo ogni iterazione è previsto 100000. Tuttavia, vi sono molte variazioni nel valore. Ciò è dovuto all'accesso simultaneo dei thread alla variabile globale condivisa x.

Produzione

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

Affrontare le condizioni di gara usando i lucchetti

Poiché abbiamo visto l'effetto della condizione di competizione nel programma sopra, abbiamo bisogno di uno strumento di sincronizzazione, che possa gestire la condizione di competizione tra più thread. In Python, il<threading>Il modulo fornisce la classe Lock per gestire le condizioni di competizione. Inoltre, ilLockclass fornisce diversi metodi con l'aiuto dei quali possiamo gestire le condizioni di competizione tra più thread. I metodi sono descritti di seguito:

metodo di acquisizione ()

Questo metodo viene utilizzato per acquisire, ovvero bloccare, un blocco. Un blocco può bloccare o non bloccare a seconda del seguente valore vero o falso:

  • With value set to True - Se il metodo acquis () viene invocato con True, che è l'argomento predefinito, l'esecuzione del thread viene bloccata fino a quando il blocco non viene sbloccato.

  • With value set to False - Se il metodo acquis () viene invocato con False, che non è l'argomento predefinito, l'esecuzione del thread non viene bloccata finché non viene impostata su true, ovvero finché non viene bloccata.

metodo release ()

Questo metodo viene utilizzato per rilasciare un blocco. Di seguito sono riportate alcune attività importanti relative a questo metodo:

  • Se un lucchetto è bloccato, il file release()metodo lo sbloccherebbe. Il suo compito è consentire a un solo thread di procedere se più di un thread è bloccato e in attesa che il blocco venga sbloccato.

  • Solleverà un ThreadError se il blocco è già sbloccato.

Ora possiamo riscrivere il programma precedente con la classe lock ei suoi metodi per evitare la condizione di competizione. Dobbiamo definire il metodo taskofThread () con l'argomento lock e quindi dobbiamo utilizzare i metodi acquis () e release () per bloccare e non bloccare i blocchi per evitare la condizione di competizione.

Esempio

Di seguito è riportato un esempio di programma Python per comprendere il concetto di blocchi per gestire le condizioni di gara -

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

L'output seguente mostra che l'effetto della race condition viene trascurato; poiché il valore di x, dopo ogni & ogni iterazione, è ora 100000, che è come previsto da questo programma.

Produzione

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 - The Dining Philosophers problem

Il deadlock è un problema problematico che si può affrontare durante la progettazione dei sistemi concorrenti. Possiamo illustrare questo problema con l'aiuto del problema del filosofo a tavola come segue:

Edsger Dijkstra ha originariamente introdotto il problema del filosofo del pranzo, una delle famose illustrazioni di uno dei più grandi problemi del sistema concorrente chiamato deadlock.

In questo problema, ci sono cinque famosi filosofi seduti a una tavola rotonda che mangiano del cibo dalle loro ciotole. Ci sono cinque forchette che possono essere usate dai cinque filosofi per mangiare il loro cibo. Tuttavia, i filosofi decidono di usare due forchette contemporaneamente per mangiare il loro cibo.

Ora, ci sono due condizioni principali per i filosofi. Primo, ciascuno dei filosofi può essere in stato di alimentazione o di pensiero e, secondo, devono prima ottenere entrambe le forchette, cioè sinistra e destra. Il problema sorge quando ciascuno dei cinque filosofi riesce a prendere contemporaneamente il bivio di sinistra. Ora tutti aspettano che la forchetta giusta sia libera, ma non rinunceranno mai alla forchetta finché non avranno mangiato il cibo e la forchetta giusta non sarà mai disponibile. Quindi, ci sarebbe uno stato di stallo a tavola.

Deadlock nel sistema concorrente

Ora, se vediamo, lo stesso problema può sorgere anche nei nostri sistemi concorrenti. I fork nell'esempio precedente sarebbero le risorse di sistema e ogni filosofo può rappresentare il processo, che è in competizione per ottenere le risorse.

Soluzione con programma Python

La soluzione di questo problema può essere trovata dividendo i filosofi in due tipi: greedy philosophers e generous philosophers. Principalmente un filosofo avido proverà a prendere la forcella sinistra e aspetterà finché non sarà lì. Quindi aspetterà che ci sia la forchetta giusta, la raccoglierà, la mangerà e poi la metterà giù. Un filosofo generoso proverà invece a prendere in mano la forcella sinistra e se non c'è, aspetterà e riproverà dopo un po 'di tempo. Se prendono la forcella sinistra, cercheranno di prendere quella giusta. Se prenderanno anche la forchetta giusta, mangeranno e rilasceranno entrambe le forchette. Tuttavia, se non otterranno la forcella destra, rilasceranno la forcella sinistra.

Esempio

Il seguente programma Python ci aiuterà a trovare una soluzione al problema del filosofo a tavola:

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

Il programma di cui sopra utilizza il concetto di filosofi avidi e generosi. Il programma ha anche utilizzato l'estensioneacquire() e release() metodi di Lock classe di <threading>modulo. Possiamo vedere la soluzione nel seguente output:

Produzione

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.