Синхронизация потоков

Синхронизация потоков может быть определена как метод, с помощью которого мы можем быть уверены, что два или более параллельных потока не обращаются одновременно к сегменту программы, известному как критическая секция. С другой стороны, как мы знаем, критическая секция - это часть программы, в которой осуществляется доступ к общему ресурсу. Следовательно, мы можем сказать, что синхронизация - это процесс обеспечения того, чтобы два или более потока не взаимодействовали друг с другом, обращаясь к ресурсам одновременно. На диаграмме ниже показано, что четыре потока одновременно пытаются получить доступ к критическому разделу программы.

Чтобы было понятнее, предположим, что два или более потока одновременно пытаются добавить объект в список. Это действие не может привести к успешному завершению, потому что оно либо отбрасывает один или все объекты, либо полностью искажает состояние списка. Здесь роль синхронизации заключается в том, что только один поток одновременно может получить доступ к списку.

Проблемы с синхронизацией потоков

Мы можем столкнуться с проблемами при реализации параллельного программирования или применении синхронизирующих примитивов. В этом разделе мы обсудим два основных вопроса. Проблемы -

  • Deadlock
  • Состояние гонки

Состояние гонки

Это одна из основных проблем параллельного программирования. Одновременный доступ к общим ресурсам может привести к состоянию гонки. Состояние гонки может быть определено как возникновение условия, когда два или более потоков могут получить доступ к совместно используемым данным, а затем пытаются одновременно изменить их значение. Из-за этого значения переменных могут быть непредсказуемыми и варьироваться в зависимости от таймингов переключения контекста процессов.

пример

Рассмотрим этот пример, чтобы понять концепцию состояния гонки -

Step 1 - На этом этапе нам нужно импортировать модуль потоковой передачи -

import threading

Step 2 - Теперь определите глобальную переменную, скажем x, вместе со значением 0 -

x = 0

Step 3 - Теперь нам нужно определить increment_global() функция, которая будет делать приращение на 1 в этой глобальной функции x -

def increment_global():

   global x
   x += 1

Step 4 - На этом этапе мы определим taskofThread()функция, которая будет вызывать функцию increment_global () указанное количество раз; для нашего примера это 50000 раз -

def taskofThread():

   for _ in range(50000):
      increment_global()

Step 5- Теперь определите функцию main (), в которой создаются потоки t1 и t2. Оба будут запущены с помощью функции start () и дождаться завершения своей работы с помощью функции 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- Теперь нам нужно указать диапазон количества итераций, которые мы хотим вызвать функцию main (). Здесь мы звоним 5 раз.

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

В выходных данных, показанных ниже, мы можем увидеть эффект состояния гонки, поскольку значение x после каждой итерации ожидается 100000. Тем не менее, есть много вариаций в значении. Это связано с одновременным доступом потоков к общей глобальной переменной x.

Вывод

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

Работа с состоянием гонки с помощью блокировок

Поскольку мы видели эффект состояния гонки в приведенной выше программе, нам нужен инструмент синхронизации, который может иметь дело с состоянием гонки между несколькими потоками. В Python<threading>Модуль предоставляет класс Lock для обработки состояния гонки. ДалееLockclass предоставляет различные методы, с помощью которых мы можем обрабатывать состояние гонки между несколькими потоками. Методы описаны ниже -

получить () метод

Этот метод используется для получения, т. Е. Блокировки блокировки. Блокировка может быть блокирующей или неблокирующей в зависимости от следующего истинного или ложного значения:

  • With value set to True - Если метод Acquire () вызывается со значением True, которое является аргументом по умолчанию, выполнение потока блокируется до тех пор, пока блокировка не будет разблокирована.

  • With value set to False - Если метод Acqua () вызывается с False, что не является аргументом по умолчанию, то выполнение потока не блокируется, пока не будет установлено значение true, то есть пока оно не будет заблокировано.

release () метод

Этот метод используется для снятия блокировки. Ниже приведены несколько важных задач, связанных с этим методом.

  • Если замок заблокирован, то release()метод разблокировал бы его. Его задача - разрешить выполнение ровно одного потока, если более одного потока заблокированы и ждут, пока блокировка не будет разблокирована.

  • Это поднимет ThreadError если замок уже разблокирован.

Теперь мы можем переписать указанную выше программу с классом блокировки и его методами, чтобы избежать состояния гонки. Нам нужно определить метод taskofThread () с аргументом блокировки, а затем нужно использовать методы Acquire () и Release () для блокировки и неблокирования блокировок, чтобы избежать состояния гонки.

пример

Ниже приведен пример программы Python, чтобы понять концепцию блокировок для работы с состоянием гонки.

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

Следующий вывод показывает, что влияние состояния гонки не учитывается; поскольку значение x после каждой и каждой итерации теперь равно 100000, что соответствует ожиданиям этой программы.

Вывод

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

Тупики - проблема обедающих философов

Тупик - это неприятная проблема, с которой можно столкнуться при проектировании параллельных систем. Мы можем проиллюстрировать эту проблему с помощью проблемы обеденного философа следующим образом:

Эдсгер Дейкстра первоначально представил проблему обеденного философа, одну из знаменитых иллюстраций одной из самых больших проблем параллельной системы, называемой тупиком.

В этой задаче пять известных философов сидят за круглым столом и поедают еду из своих мисок. Пять философов могут использовать пять вилок для еды. Однако философы решают использовать две вилки одновременно, чтобы съесть свою пищу.

Итак, для философов есть два основных условия. Во-первых, каждый из философов может находиться либо в состоянии еды, либо в состоянии мышления, и, во-вторых, они сначала должны получить обе вилки, то есть левую и правую. Проблема возникает, когда каждому из пяти философов удается одновременно выбрать левую вилку. Теперь все они ждут, пока будет свободна нужная вилка, но они никогда не откажутся от вилки, пока не съедят свою еду, и нужная вилка никогда не будет доступна. Следовательно, за обеденным столом возникнет тупиковая ситуация.

Тупик в параллельной системе

Теперь, если мы увидим, та же проблема может возникнуть и в наших параллельных системах. Разветвлениями в приведенном выше примере будут системные ресурсы, и каждый философ может представлять процесс, который конкурирует за ресурсы.

Решение с программой Python

Решение этой проблемы можно найти, разделив философов на два типа: greedy philosophers и generous philosophers. В основном жадный философ будет пытаться взять левую вилку и ждать, пока она там не окажется. Затем он будет ждать, пока там окажется нужная вилка, возьмет ее, съест, а затем положит. С другой стороны, великодушный философ попытается подобрать левую вилку, и если ее там нет, он подождет и попробует снова через некоторое время. Если они получат левую вилку, они попытаются получить правую. Если они получат и правильную вилку, они съедят и отпустят обе вилки. Однако, если они не получат правую вилку, они освободят левую вилку.

пример

Следующая программа Python поможет нам найти решение проблемы обеденного философа -

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

В приведенной выше программе используется концепция жадных и щедрых философов. Программа также использовалаacquire() и release() методы Lock класс <threading>модуль. Мы можем увидеть решение в следующем выводе -

Вывод

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.