Поиск по сайту:

Как создать семафор в Bash


Вы разрабатываете многопоточное приложение? Рано или поздно вам, вероятно, понадобится использовать семафор. В этой статье вы узнаете, что такое семафор, как создать/реализовать его в Bash и многое другое.

Что такое семафор?

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

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

Остановившись на некоторое время в Нидерландах, стране, полной небольших водных путей и множества разводных мостов (называемых drawbridge в американском английском), можно увидеть множество отличных реальных примеров семафора; рассмотрим ручку оператора разводного моста: вверх или вниз. Эта ручка представляет собой семафорную переменную, защищающую либо водный путь, либо дорогу от несчастных случаев. Таким образом, водный путь и дороги можно рассматривать как другие переменные, защищенные семафором.

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

Переменная семафора может управлять доступом к другому набору переменных. Например, рабочее состояние дескриптора предотвращает обновление переменных $cars_per_minute и $car_toll_booth_total_income и т. д.

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

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

В данном случае это применимо к нашему примеру, поскольку оператор моста является единственным, кто имеет контроль над нашим семафором и дескриптором включения/выключения мьютекса. Напротив, если бы в караульном помещении в конце моста был переключатель блокировки моста, у нас все еще был бы семафор, но не мьютекс.

Обе конструкции регулярно используются в компьютерном программировании, когда используется несколько потоков, чтобы обеспечить доступ только одного процесса или задачи к данному ресурсу в любое время. Некоторые семафоры могут иметь сверхбыстрое переключение, например, при использовании в многопоточном программном обеспечении для торговли на финансовых рынках, а некоторые могут быть намного медленнее, меняя состояние только каждые несколько минут, например, при использовании в автоматизированном подъемном мосту или при пересечении автопоезда.

Теперь, когда мы лучше понимаем семафоры, давайте реализуем их в Bash.

Реализация семафора в Bash: легко или нет?

Реализация семафора в Bash настолько проста, что это можно сделать даже прямо из командной строки, или так кажется…

Начнем с простого.

BRIDGE=up
if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi
BRIDGE=down
if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi

В этом коде переменная BRIDGE содержит статус моста. Когда мы устанавливаем вверх, корабли могут проходить, а когда мы устанавливаем вниз, могут проходить автомобили. Мы также можем прочитать значение нашей переменной в любой момент, чтобы увидеть, действительно ли мост поднят или опущен. Общий/общий ресурс в данном случае — это наш мост.

Однако этот пример является однопоточным, поэтому мы никогда не сталкивались с ситуацией требуется семафор. Другой способ думать об этом состоит в том, что наша переменная никогда не может быть up и down в один и тот же момент времени, когда код выполняется последовательно, т. е. шаг за шагом.

Следует также отметить, что мы на самом деле не контролировали доступ к другой переменной (как это обычно делает семафор), поэтому наша переменная BRIDGE на самом деле не является настоящей переменной семафора, хотя и близка к ней.

Наконец, как только мы вводим несколько потоков, которые могут повлиять на переменную BRIDGE, мы сталкиваемся с проблемами. Например, что, если сразу после команды BRIDGE=up другой поток выдает BRIDGE=down, в результате чего появляется сообщение Автомобили могут проехать!, несмотря на то, что первый поток ожидал, что мост будет up, а на самом деле мост все еще движется. Опасный!

Вы можете видеть, как все может быстро стать неясным и запутанным, не говоря уже о сложном, при работе с несколькими потоками.

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

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

Создание семафора Bash

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

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

В нашем небольшом примере мы рассмотрим определение семафора Bash, когда один из операторов разводного моста опускает ручку моста, тем самым показывая, что он или она хочет опустить мост. Внимательные читатели могли заметить ссылку на operators вместо operator: теперь есть несколько операторов, которые могут опустить мост. Другими словами, существует несколько потоков или задач, которые выполняются одновременно.

#!/bin/bash

BRIDGE_SEMAPHORE=0

lower_bridge(){  # An operator put one of the bridge operation handles downward (as a new state).
  # Assume it was previously agreed between operators that as soon as one of the operators 
  # moves a bridge operation handle that their command has to be executed, either sooner or later
  # hence, we commence a loop which will wait for the bridge to become available for movement
  while true; do
    if [ "${BRIDGE_SEMAPHORE}" -eq 1 ]; then
      echo "Bridge semaphore locked, bridge moving or other issue. Waiting 2 minutes before re-check."
      sleep 120
      continue  # Continue loop
    elif [ "${BRIDGE_SEMAPHORE}" -eq 0 ]; then   
      echo "Lower bridge command accepted, locking semaphore and lowering the bridge."
      BRIDGE_SEMAPHORE=1
      execute_lower_bridge
      wait_for_bridge_to_come_down
      BRIDGE='down'
      echo "Bridge lowered, ensuring at least 5 minutes pass before next allowed bridge movement."
      sleep 300
      echo "5 Minutes passed, unlocking semaphore (releasing bridge control)"
      BRIDGE_SEMAPHORE=0
      break  # Exit loop
    fi
  done
}

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

На самом деле, эта функция закончила поднимать мост, но ввела обязательное 5-минутное ожидание, о котором заранее договорились все операторы и которое было жестко прописано в исходном коде: оно предотвращает постоянное поднятие/опускание моста. Вы также можете увидеть это обязательное 5-минутное ожидание, реализованное в этой функции как sleep 300.

Итак, когда эта функция raise_bridge работает, она установит для переменной семафора BRIDGE_SEMAPHORE значение 1, точно так же, как мы делаем в этом коде (сразу после echo \Команда опускания моста принята, блокировка семафора и команда опускания моста\), и – с помощью первой условной проверки if в этом коде – бесконечный цикл, присутствующий в этой функции, будет continue (ссылка continue в коде) на цикл с паузами в 2 минуты, поскольку переменная BRIDGE_SEMAPHORE равна 1.

Как только эта функция raise_bridge закончит поднимать мост и завершит пятиминутный сон, она установит для BRIDGE_SEMAPHORE значение 0, что позволит нашей функции lower_bridge co начать выполнение функций execute_lower_bridge и последующего wait_for_bridge_to_come_down, предварительно повторно заблокировав наш семафор на 1, чтобы другие функции не взяли на себя управление мостом.

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

Команда \Опустить мост принята, блокировка семафора и опускание моста\ не является потокобезопасной!

Если другой поток, например raise_bridge, выполняется в то же время и пытается получить доступ к переменной BRIDGE_SEMAPHORE, это может быть (когда BRIDGE_SEMAPHORE=0 и оба запущенных потока достигают своих соответствующих echo в то же самое время, когда операторы моста видят «Команда нижнего моста принята, блокировка семафора и опускание моста» и Команда поднять мост принята, блокировка семафора и подъем моста. Сразу друг за другом на экране! Страшно, не так ли?

Еще более пугающим является тот факт, что оба потока могут перейти к BRIDGE_SEMAPHORE=1, и оба потока могут продолжить выполнение! (Ничто не мешает им это сделать) Причина в том, что для таких сценариев пока не так много защиты. Хотя этот код реализует семафор, он ни в коем случае не является потокобезопасным. Как уже говорилось, многопоточное кодирование сложно и требует большого опыта.

Хотя время, необходимое в этом случае, минимально (1-2 строки кода выполняются всего за несколько миллисекунд), и, учитывая вероятное небольшое количество операторов моста, вероятность того, что это произойдет, очень мала. Однако тот факт, что это возможно, делает его опасным. Создание потокобезопасного кода в Bash — непростая задача.

Это может быть дополнительно улучшено, например, введением предварительной блокировки и/или введением некоторой формы задержки с последующей повторной проверкой (хотя для этого, вероятно, потребуется дополнительная переменная) или регулярной повторной проверкой перед фактическим выполнением моста. и т. д. Другой вариант — сделать приоритетную очередь или переменную-счетчик, которая проверяет, сколько потоков заблокировали управление мостом и т. д.

Другой часто используемый подход, например, при запуске нескольких сценариев bash, которые могут взаимодействовать, заключается в использовании mkdir или flock в качестве операций базовой блокировки. В Интернете доступны различные примеры их реализации, например, «Какие команды Unix можно использовать в качестве семафора/блокировки?».

Подведение итогов

В этой статье мы рассмотрим, что такое семафор. Мы также кратко коснулись темы мьютекса. Наконец, мы рассмотрели реализацию семафора в Bash на практическом примере нескольких операторов моста, управляющих разводным/подъемным мостом. Мы также изучили, насколько сложна реализация надежного решения на основе семафоров.

Если вам понравилось читать эту статью, взгляните на наши утверждения, ошибки и сбои: в чем разница? статья.