Алгоритмы взаимного исключения

Материал из Викиконспекты
Версия от 12:12, 25 сентября 2018; Kirill.vakhrushev (обсуждение | вклад) (Взаимное исключение)
Перейти к: навигация, поиск

Определения

Определение:
Взаимное исключение (англ. mutual exclusion) — свойство построения параллельных программ, которое используется в целях предотвращения состояния гонки (англ. race condition); Оно требует, чтобы один поток исполнения никогда не входил в критическую секцию одновременно с тем, как другой параллельный поток выполнения вошел в свою критическую секцию.

То есть критические секции не могут выполняться параллельно: [math]\forall i,j:i \neq j \Rightarrow CS_i \rightarrow CS_j \vee CS_j \rightarrow CS_i [/math]. Это значит, что выполнение критических секций будет линеаризуемо. Это требование корректности протокола взаимной блокировки.


Определение:
Критическая секция (англ. critical section) — участок исполняемого кода программы, в котором производится доступ к общему ресурсу (данным или устройству), который не должен быть одновременно использован более чем одним потоком исполнения.

Проблема

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

Успешное решение этой проблемы должно иметь по крайней мере три свойства:

условие корректности
  1. Взаимное исключение: только один поток может быть в критической секции.
условия прогресса
  1. Отсутствие взаимоблокировок (англ. deadlocks): если несколько потоков пытаются войти в критическую секцию, то хотя бы один из них должен войти в критическую секцию за конечное время.
  2. Отсутствие голодания (англ. starvation-freedom): если какой-то поток пытается войти в критическую секцию, то он войдет в критическую секцию за конечное время.


Каждая программа может быть разделена на четыре секции, что приводит к четырем состояниям.
Порядок перехода между состояниями
Non-Critical Section
Операция находится вне критической секции; этот процесс не использует или не запрашивает общий ресурс.
Trying
Процесс пытается войти в критический раздел.
Critical Section
В этом разделе разрешен доступ к общему ресурсу.
Exit 
Процесс выходит из критического раздела и делает доступный общий ресурс другим процессам.

Если процесс хочет войти в критический раздел, он должен сначала выполнить раздел [math]try[/math] и подождать, пока он не получит доступ к критическому разделу. После того, как процесс выполнил свой критический раздел и завершился с общими ресурсами, ему необходимо выполнить раздел выхода, чтобы освободить их для использования другими процессами. Затем процесс возвращается в некритический раздел.

Алгоритмы взаимного исключения

Алгоритм Петерсона для [math]2[/math] потоков

Простейший алгоритм параллельного программирования для взаимного исключения потоков исполнения кода, разработанный Гарри Петерсоном в [math]1981[/math] г. Хотя изначально был сформулирован для 2-поточного случая, алгоритм может быть обобщён для произвольного количества потоков. Гарантирует взаимное исключение, отсутствие взаимной блокировки и отсутствие голодания.

Принцип работы: перед тем как начать исполнение критической секции кода, поток должен вызвать процедуру [math]lock()[/math] со своим номером в качестве параметра. Она должна организовать ожидание потоком своей очереди входа в критическую секцию. После исполнения критической секции и выхода из неё поток вызывает другую процедуру [math]unlock()[/math], после чего уже другие потоки смогут войти в критическую область. Посмотрим, как реализуется этот общий принцип алгоритмом Петерсона для двух потоков.

  threadlocal int id  // 0 or 1
  shared int want[2]
  shared int victim
  def lock:
       want[id] = true
       victim = id
       while want[1-id] and
             victim == id:
          pass
  
  def unlock:
    want[id] = false

Корректность алгоритма

Взаимное исключение

Потоки [math]0[/math] и [math]1[/math] никогда не могут попасть в критическую секцию в один момент времени: если [math]0[/math] вошёл в секцию, он установил [math]want[0][/math] в [math]true[/math]. Тогда либо [math]want[1] = false[/math] (тогда поток [math]1[/math] не в критической секции), либо [math]waiting = 1[/math] (тогда поток [math]1[/math] пытается войти в критическую секцию и крутится в цикле), либо поток [math]1[/math] пытается войти в критическую секцию после установки [math]want[1] = true[/math], но до установки [math]waiting[/math] и цикла. Таким образом, если оба процесса находятся в критической секции, должно быть [math] want[0] \space and \space want[1] \space and \space waiting = 0 \space and \space waiting = 1 [/math], но такого не может быть одновременно и мы пришли к противоречию.

Свобода от взаимной блокировки

Для того, чтобы оба процесса находились в ожидании, необходимы противоположные значения переменной [math]waiting[/math], что невозможно.

Свобода от голодания

Возможна ситуация, когда один процесс будет несколько раз подряд захватывать себе критическую секцию, а другой, изъявивший желание попасть туда, будет ждать. В алгоритме Петерсона процесс не будет ждать дольше, чем один вход в критическую секцию: после выполнения [math]unlock()[/math] и повторного захода в [math]lock()[/math] процесс установит себя как ждущего и попадёт в цикл, который не завершится, пока не отработает другой процесс.

Алгоритм Петерсона для [math]N[/math] потоков

Обобщение Алгоритм Петерсона для [math]N[/math] потоков. Гарантирует взаимное исключение, отсутствие блокировки и отсутствие голодания. Но алгоритм не очень честный. Невезучий поток может ждать пока другие потоки [math]O(N^2)[/math] раз войдут в критическую секцию (квадратичное ожидание).

  threadlocal int id  // 0 to N-1
  shared int level[N]
  shared int victim[N]
  def lock: 
    for j = 1..N-1:  //Для входа в CS надо пройти на N-1 уровней
       level[id] = j  //Обобщаем want на уровень j: level[id] >= j
       victim[j] = id  //Своя жертва на каждом уровне
       while exist k: k != id and  //Для прохода на следующий уровень соревнуемся со всеми другими потоками
             level[k] >= j and
             victim[j] == id:
          pass
  
  def unlock:
    level[id] = 0

Алгоритм Лампорта (вариант [math]1[/math])

Алгоритм пекарни Лампорта алгоритм разделения общих ресурсов между несколькими потоками путём взаимного исключения. Опубликован Лесли Лампортом в 1974 году.

Лампорт предлагает рассмотреть пекарню с устройством, выдающим номерки у входа. Каждому входящему покупателю выдаётся номерок на единицу больше предыдущего. Общий счётчик показывает номер обслуживаемого в данный момент клиента. Все остальные покупатели ждут, пока не закончат обслуживать текущего клиента и табло покажет следующий номер. После того как клиент сделает покупку и сдаст свой номерок, служащий увеличивает на единицу допустимые для выдачи устройством у входа номера. Если совершивший покупку клиент захочет снова что-нибудь купить, он должен будет снова взять номерок у входа и встать в общую очередь.

Пусть покупатели это потоки, получившие номера [math]i[/math] от глобальной переменной. При наличии нескольких потоков, получивших номер [math]n[/math] при входе в критическую секцию, поток с меньшим номером [math]i[/math] будет иметь больший приоритет при обслуживании (входе в критическую секцию).

Ключевое свойство: если поток [math]P[/math] выполнил [math]doorway (DW)[/math] до потока [math]Q[/math], то у него более ранний номер очереди.

  threadlocal int id  // 0 to N-1
  shared boolean want[N] init false
  shared int label[N] init 0
  def lock:
    want[id] = true
    label[id] = max(label) + 1
    while exists k: k != id and
          want[k] and
          (label[k], k) < (label[id], id) :
       pass
  def unlock:
    want[id] = false

Обладает свойством первый пришел, первый обслужен (FCFS). Это сильнее чем линейное ожидание, cамое сильное свойство прогресса.

First Come First Served (FCFS)

Требование [math]First Come First Served[/math] формализуется так:

  1. Метод [math]lock[/math] должен состоять из двух последовательных секций.
  def lock():
     doorway
     waitnig
  1. Секция [math]doorway[/math] должны быть [math]wait free[/math], то есть выполняться за конечное число шагов, независимо от других потоков.
  2. Секция [math]waiting[/math] должна выполнять условие: Если [math]DW_i \Rightarrow DW_j[/math], то [math]res(WT_i) \Rightarrow res(WT_j)[/math].

Алгоритм Лампорта (вариант [math]2[/math])

Обладает всеми свойствами классического варианта, но дополнительно позволяет решить проблему с бесконечными метками.

  threadlocal int id  // 0 to N-1
  shared boolean want[N] init false
  shared int label[N] init 0
  def lock:
    choosing[id] = true
    label[id] = max(label!=inf) + 1
    choosing[id] = false
    while exists k: k != id and
          (choosing[k] or
          (label[k], k) < (label[id], id)) :
       pass
  def unlock:
    label[id] = inf

См. также

Источники информации