Sincronizando Threads

A sincronização de threads pode ser definida como um método com a ajuda do qual podemos ter certeza de que duas ou mais threads simultâneas não estão acessando simultaneamente o segmento do programa conhecido como seção crítica. Por outro lado, como sabemos, essa seção crítica é a parte do programa onde o recurso compartilhado é acessado. Portanto, podemos dizer que a sincronização é o processo de garantir que dois ou mais threads não façam interface entre si acessando os recursos ao mesmo tempo. O diagrama abaixo mostra que quatro threads estão tentando acessar a seção crítica de um programa ao mesmo tempo.

Para tornar isso mais claro, suponha que dois ou mais threads tentem adicionar o objeto na lista ao mesmo tempo. Este ato não pode levar a um final bem-sucedido, porque eliminará um ou todos os objetos ou corromperá completamente o estado da lista. Aqui, o papel da sincronização é que apenas um thread de cada vez pode acessar a lista.

Problemas na sincronização do thread

Podemos encontrar problemas ao implementar programação simultânea ou aplicar primitivas de sincronização. Nesta seção, discutiremos duas questões principais. Os problemas são -

  • Deadlock
  • Condição de corrida

Condição de corrida

Este é um dos principais problemas da programação simultânea. O acesso simultâneo a recursos compartilhados pode levar a uma condição de corrida. Uma condição de corrida pode ser definida como a ocorrência de uma condição em que dois ou mais threads podem acessar dados compartilhados e, em seguida, tentar alterar seu valor ao mesmo tempo. Devido a isso, os valores das variáveis ​​podem ser imprevisíveis e variam dependendo dos tempos de troca de contexto dos processos.

Exemplo

Considere este exemplo para entender o conceito de condição de corrida -

Step 1 - Nesta etapa, precisamos importar o módulo de threading -

import threading

Step 2 - Agora, defina uma variável global, digamos x, junto com seu valor como 0 -

x = 0

Step 3 - Agora, precisamos definir o increment_global() função, que fará o incremento de 1 nesta função global x -

def increment_global():

   global x
   x += 1

Step 4 - Nesta etapa, vamos definir o taskofThread()função, que irá chamar a função increment_global () por um determinado número de vezes; para o nosso exemplo é 50000 vezes -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- Agora, defina a função main () na qual os threads t1 e t2 são criados. Ambos serão iniciados com a ajuda da função start () e aguardarão até que concluam seus trabalhos com a ajuda da função 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- Agora, precisamos fornecer o intervalo para quantas iterações queremos chamar a função main (). Aqui, estamos chamando por 5 vezes.

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

Na saída mostrada abaixo, podemos ver o efeito da condição de corrida, pois o valor de x após cada iteração é esperado 100000. No entanto, há muita variação no valor. Isso se deve ao acesso simultâneo de threads à variável global compartilhada x.

Resultado

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

Lidar com a condição de corrida usando bloqueios

Como vimos o efeito da condição de corrida no programa acima, precisamos de uma ferramenta de sincronização, que pode lidar com a condição de corrida entre vários threads. Em Python, o<threading>módulo fornece classe Lock para lidar com condições de corrida. Além disso, oLockclasse fornece métodos diferentes com a ajuda dos quais podemos lidar com a condição de corrida entre vários segmentos. Os métodos são descritos abaixo -

método adquirir ()

Este método é usado para adquirir, ou seja, bloquear um bloqueio. Um bloqueio pode ser bloqueador ou não, dependendo do seguinte valor verdadeiro ou falso -

  • With value set to True - Se o método occur () for chamado com True, que é o argumento padrão, a execução do thread é bloqueada até que o bloqueio seja desbloqueado.

  • With value set to False - Se o método locate () é chamado com False, que não é o argumento padrão, a execução da thread não é bloqueada até que seja definida como true, ou seja, até que seja bloqueada.

método release ()

Este método é usado para liberar um bloqueio. A seguir estão algumas tarefas importantes relacionadas a este método -

  • Se uma fechadura estiver trancada, então o release()método iria desbloqueá-lo. Seu trabalho é permitir que exatamente um thread prossiga se mais de um thread estiver bloqueado e esperando que o bloqueio seja desbloqueado.

  • Vai levantar um ThreadError se o bloqueio já estiver desbloqueado.

Agora, podemos reescrever o programa acima com a classe de bloqueio e seus métodos para evitar a condição de corrida. Precisamos definir o método taskofThread () com o argumento lock e, em seguida, usar os métodos activate () e release () para bloquear e não bloquear bloqueios para evitar condição de corrida.

Exemplo

A seguir está um exemplo de programa python para entender o conceito de bloqueios para lidar com a condição de corrida -

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

A saída a seguir mostra que o efeito da condição de corrida é negligenciado; já que o valor de x, após cada & cada iteração, é agora 100000, que está de acordo com a expectativa deste programa.

Resultado

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 - O problema dos Dining Philosophers

O deadlock é uma questão problemática que pode ser enfrentada ao projetar os sistemas concorrentes. Podemos ilustrar esse problema com a ajuda do problema do filósofo jantando da seguinte maneira -

Edsger Dijkstra originalmente introduziu o problema do filósofo jantando, uma das famosas ilustrações de um dos maiores problemas de sistema concorrente chamado impasse.

Neste problema, há cinco filósofos famosos sentados em uma mesa redonda comendo um pouco de comida de suas tigelas. Existem cinco garfos que podem ser usados ​​pelos cinco filósofos para comerem. No entanto, os filósofos decidem usar dois garfos ao mesmo tempo para comer sua comida.

Agora, existem duas condições principais para os filósofos. Primeiro, cada um dos filósofos pode estar comendo ou pensando e, segundo, eles devem primeiro obter os dois garfos, isto é, esquerdo e direito. A questão surge quando cada um dos cinco filósofos consegue escolher a bifurcação esquerda ao mesmo tempo. Agora todos eles estão esperando que o garfo certo seja liberado, mas nunca abrirão mão de seu garfo até que tenham comido e o garfo certo nunca estará disponível. Conseqüentemente, haveria um estado de impasse na mesa de jantar.

Deadlock no sistema concorrente

Agora, se virmos, o mesmo problema pode surgir em nossos sistemas concorrentes também. As bifurcações no exemplo acima seriam os recursos do sistema e cada filósofo pode representar o processo, que está competindo para obter os recursos.

Solução com programa Python

A solução deste problema pode ser encontrada dividindo os filósofos em dois tipos - greedy philosophers e generous philosophers. Principalmente um filósofo ganancioso tentará pegar o garfo esquerdo e esperar até que ele esteja lá. Ele irá então esperar que o garfo certo esteja lá, pegá-lo, comer e então largá-lo. Por outro lado, um filósofo generoso tentará pegar a bifurcação da esquerda e, se ela não estiver lá, esperará e tentará novamente depois de algum tempo. Se eles pegarem a bifurcação da esquerda, eles tentarão pegar a direita. Se eles pegarem o garfo certo também, eles comerão e soltarão os dois garfos. No entanto, se eles não conseguirem o garfo direito, eles irão liberar o garfo esquerdo.

Exemplo

O seguinte programa Python nos ajudará a encontrar uma solução para o problema do filósofo jantando -

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

O programa acima usa o conceito de filósofos gananciosos e generosos. O programa também usou oacquire() e release() métodos do Lock classe do <threading>módulo. Podemos ver a solução na seguinte saída -

Resultado

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.