Учебники

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

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

синхронизирующий

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

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

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

  • тупик
  • Состояние гонки

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

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

пример

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

Шаг 1 — На этом шаге нам нужно импортировать модуль потоков —

import threading

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

x = 0

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

def increment_global():

   global x
   x += 1

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

def taskofThread():

   for _ in range(50000):
      increment_global()

Шаг 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()

Шаг 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 для работы с условиями гонки. Кроме того, класс Lock предоставляет различные методы, с помощью которых мы можем обрабатывать состояние гонки между несколькими потоками. Методы описаны ниже —

приобретать () метод

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

  • Со значением, установленным в True — если метод acqu () вызывается с True, который является аргументом по умолчанию, то выполнение потока блокируется до тех пор, пока блокировка не будет разблокирована.

  • При значении False — если метод acqu () вызывается с False, который не является аргументом по умолчанию, то выполнение потока не блокируется, пока не будет установлено значение true, т. Е. До тех пор, пока он не будет заблокирован.

Со значением, установленным в True — если метод acqu () вызывается с True, который является аргументом по умолчанию, то выполнение потока блокируется до тех пор, пока блокировка не будет разблокирована.

При значении False — если метод acqu () вызывается с False, который не является аргументом по умолчанию, то выполнение потока не блокируется, пока не будет установлено значение true, т. Е. До тех пор, пока он не будет заблокирован.

метод release ()

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

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

  • Он вызовет ThreadError, если блокировка уже разблокирована.

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

Он вызовет ThreadError, если блокировка уже разблокирована.

Теперь мы можем переписать вышеупомянутую программу с помощью класса блокировки и его методов, чтобы избежать условия гонки. Нам нужно определить метод taskofThread () с аргументом блокировки, а затем использовать методы acqu () и 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

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

пример

Следующая программа на 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()

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