Synchronizacja wątków

Synchronizację wątków można zdefiniować jako metodę, za pomocą której możemy być pewni, że dwa lub więcej współbieżnych wątków nie uzyskuje jednocześnie dostępu do segmentu programu znanego jako sekcja krytyczna. Z drugiej strony, jak wiemy, ta sekcja krytyczna to część programu, w której uzyskuje się dostęp do współdzielonego zasobu. Dlatego możemy powiedzieć, że synchronizacja jest procesem zapewniającym, że dwa lub więcej wątków nie będzie ze sobą współpracować, uzyskując dostęp do zasobów w tym samym czasie. Poniższy diagram pokazuje, że cztery wątki próbują uzyskać dostęp do krytycznej sekcji programu w tym samym czasie.

Aby było jaśniej, załóżmy, że dwa lub więcej wątków próbuje jednocześnie dodać obiekt do listy. Ta czynność nie może doprowadzić do pomyślnego zakończenia, ponieważ spowoduje usunięcie jednego lub wszystkich obiektów lub całkowicie zepsuje stan listy. W tym przypadku rola synchronizacji polega na tym, że tylko jeden wątek na raz ma dostęp do listy.

Problemy z synchronizacją wątków

Możemy napotkać problemy podczas wdrażania programowania współbieżnego lub stosowania synchronizujących elementów podstawowych. W tej sekcji omówimy dwie główne kwestie. Problemy są -

  • Deadlock
  • Warunki wyścigu

Warunki wyścigu

Jest to jeden z głównych problemów w programowaniu współbieżnym. Jednoczesny dostęp do współdzielonych zasobów może prowadzić do wyścigu. Stan wyścigu można zdefiniować jako wystąpienie warunku, w którym dwa lub więcej wątków może uzyskać dostęp do udostępnionych danych, a następnie spróbować jednocześnie zmienić ich wartość. Z tego powodu wartości zmiennych mogą być nieprzewidywalne i różnić się w zależności od czasów przełączania kontekstu procesów.

Przykład

Rozważ ten przykład, aby zrozumieć pojęcie warunków wyścigu -

Step 1 - W tym kroku musimy zaimportować moduł Threading -

import threading

Step 2 - Teraz zdefiniuj zmienną globalną, powiedzmy x, wraz z jej wartością jako 0 -

x = 0

Step 3 - Teraz musimy zdefiniować increment_global() funkcja, która dokona przyrostu o 1 w tej globalnej funkcji x -

def increment_global():

   global x
   x += 1

Step 4 - W tym kroku zdefiniujemy plik taskofThread()funkcja, która wywoła funkcję Increment_global () określoną liczbę razy; dla naszego przykładu jest to 50000 razy -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- Teraz zdefiniuj funkcję main (), w której tworzone są wątki t1 i t2. Oba zostaną uruchomione za pomocą funkcji start () i zaczekają, aż zakończą swoją pracę za pomocą funkcji 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- Teraz musimy podać zakres, z iloma iteracjami chcemy wywołać funkcję main (). Tutaj wzywamy to 5 razy.

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

Na poniższym wyjściu możemy zobaczyć efekt wyścigu, ponieważ wartość x po każdej iteracji jest spodziewana jako 100000. Jednak istnieje wiele odchyleń wartości. Wynika to z równoczesnego dostępu wątków do wspólnej zmiennej globalnej x.

Wynik

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

Radzenie sobie z sytuacją wyścigu za pomocą blokad

Jak widzieliśmy wpływ stanu wyścigu w powyższym programie, potrzebujemy narzędzia do synchronizacji, które poradzi sobie z sytuacją wyścigu między wieloma wątkami. W Pythonie<threading>moduł zapewnia klasę Lock do radzenia sobie z sytuacją wyścigu. PonadtoLockclass udostępnia różne metody, za pomocą których możemy obsłużyć wyścig między wieloma wątkami. Metody opisano poniżej -

metoda nabycia ()

Ta metoda służy do pozyskiwania, czyli blokowania zamka. Blokada może blokować lub nie blokować w zależności od następującej wartości prawda lub fałsz -

  • With value set to True - Jeśli metoda acquiringu () zostanie wywołana z wartością True, która jest argumentem domyślnym, wykonywanie wątku jest blokowane do momentu odblokowania blokady.

  • With value set to False - Jeśli metoda acquiringu () zostanie wywołana z wartością False, która nie jest argumentem domyślnym, wykonanie wątku nie jest blokowane, dopóki nie zostanie ustawione na true, tj. Dopóki nie zostanie zablokowane.

release (), metoda

Ta metoda służy do zwolnienia blokady. Poniżej znajduje się kilka ważnych zadań związanych z tą metodą -

  • Jeśli blokada jest zablokowana, plik release()metoda odblokuje to. Jego zadaniem jest zezwolenie na działanie dokładnie jednego wątku, jeśli więcej niż jeden wątek jest zablokowany i czeka na odblokowanie blokady.

  • Podniesie ThreadError jeśli zamek jest już odblokowany.

Teraz możemy przepisać powyższy program z klasą lock i jej metodami, aby uniknąć sytuacji wyścigu. Musimy zdefiniować metodę taskofThread () z argumentem lock, a następnie musimy użyć metod pozyskiwania () i release () do blokowania i nieblokowania blokad, aby uniknąć sytuacji wyścigu.

Przykład

Poniżej znajduje się przykład programu w języku Python do zrozumienia koncepcji blokad do radzenia sobie z sytuacją wyścigu -

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

Poniższe dane wyjściowe pokazują, że wpływ warunków wyścigu jest pomijany; ponieważ wartość x, po każdej & każdej iteracji, wynosi teraz 100000, co jest zgodne z oczekiwaniami tego programu.

Wynik

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

Deadlock to kłopotliwa kwestia, z którą można się spotkać podczas projektowania systemów współbieżnych. Możemy zilustrować tę kwestię za pomocą problemu filozofa jadalni w następujący sposób:

Edsger Dijkstra pierwotnie przedstawił problem filozofa jedzenia, jedną ze słynnych ilustracji jednego z największych problemów współbieżnego systemu zwanego impasem.

W tym problemie pięciu znanych filozofów siedzi przy okrągłym stole i zajada do miski. Pięciu filozofów może użyć pięciu widelców do jedzenia jedzenia. Jednak filozofowie decydują się na jednoczesne użycie dwóch widelców do jedzenia.

Otóż, są dwa główne warunki dla filozofów. Po pierwsze, każdy z filozofów może być w stanie jedzenia lub myślenia, a po drugie, musi najpierw uzyskać oba rozwidlenia, tj. Lewy i prawy. Problem pojawia się, gdy każdemu z pięciu filozofów udaje się jednocześnie wybrać lewy widełki. Teraz wszyscy czekają, aż właściwy widelec będzie wolny, ale nigdy nie zrezygnują ze swojego widelca, dopóki nie zjedzą jedzenia, a właściwy widelec nigdy nie będzie dostępny. W związku z tym przy stole obiadowym nastąpiłby impas.

Impas w systemie współbieżnym

Teraz, jeśli widzimy, ten sam problem może pojawić się również w naszych współbieżnych systemach. Rozwidlenia w powyższym przykładzie byłyby zasobami systemowymi, a każdy filozof może reprezentować proces, który konkuruje o zasoby.

Rozwiązanie z programem w języku Python

Rozwiązanie tego problemu można znaleźć, dzieląc filozofów na dwa typy - greedy philosophers i generous philosophers. Głównie chciwy filozof będzie próbował chwycić lewy widelec i poczekać, aż się tam znajdzie. Następnie będzie czekał, aż pojawi się właściwy widelec, podniesie go, zje, a następnie odłoży. Z drugiej strony hojny filozof spróbuje podnieść lewy widelec, a jeśli go tam nie ma, zaczeka i spróbuje ponownie za jakiś czas. Jeśli zdobędą lewy widelec, spróbują zdobyć prawy. Jeśli dostaną również odpowiedni widelec, zjedzą i zwolnią oba widelce. Jeśli jednak nie dostaną prawego widelca, zwolnią lewy.

Przykład

Poniższy program w Pythonie pomoże nam znaleźć rozwiązanie problemu kulinarnego filozofa -

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

Powyższy program wykorzystuje koncepcję chciwych i hojnych filozofów. Program wykorzystał również rozszerzenieacquire() i release() metody Lock klasa <threading>moduł. Rozwiązanie możemy zobaczyć na następującym wyjściu -

Wynik

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.