Семафор (программирование)

Материал из Википедии — свободной энциклопедии
(перенаправлено с «Семафор (информатика)»)
Перейти к навигации Перейти к поиску

Семафо́р (англ. semaphore) — примитив синхронизации[1] работы процессов и потоков, в основе которого лежит счётчик, над которым можно производить две атомарные операции: увеличение и уменьшение значения на единицу, при этом операция уменьшения для нулевого значения счётчика является блокирующейся[2]. Служит для построения более сложных механизмов синхронизации[1] и используется для синхронизации параллельно работающих задач, для защиты передачи данных через разделяемую память, для защиты критических секций, а также для управления доступом к аппаратному обеспечению.

Легковесный семафор — механизм, позволяющий в ряде случаев уменьшить количество системных вызовов за счёт использования активного цикла ожидания в течение некоторого времени перед выполнением блокировки[3].

Семафоры могут быть двоичными и вычислительными. Вычислительные семафоры могут принимать целочисленные неотрицательные значения и используются для работы с ресурсами, количество которых ограничено, либо участвуют в синхронизации параллельно исполняемых задач. Двоичные семафоры являются частным случаем вычислительного семафора и могут принимать только два значения: 0 и 1[3][4].

Мьютексные семафоры[3] или мьютексы — упрощённая реализация семафоров, аналогичная двоичным семафорам с тем отличием, что мьютексы должны отпускаться тем же процессом или потоком, который осуществляет их захват[5]. Наряду с двоичными семафорами используются в организации критических участков кода[4][⇨]. В отличие от двоичных семафоров, начальное состояние мьютекса не может быть захваченным[6] и они могут поддерживать наследование приоритетов[7][⇨].

Содержание

Общие сведения[править | править код]

Понятие семафора было введено в 1965 году нидерландским учёным Эдсгером Дейкстрой[4], а в 1968 году он предложил использовать два семафора для решения задачи производителя и потребителя[8][⇨].

Семафор представляет собой счётчик, над которым можно выполнять две операции: увеличение на 1 (англ. up) и уменьшение на 1 (англ. down). При попытке уменьшения семафора, значение которого равно нулю, задача, запросившая данное действие, должна блокироваться до тех пор, пока не станет возможным уменьшение значения семафора до неотрицательного значения, то есть пока другой процесс не увеличит значение семафора[4]. Под блокированием задачи понимается изменение состояния процесса или потока планировщиком задач на такое, при котором задача приостановит своё исполнение[9].

Операции уменьшения и увеличения значения семафора первоначально обозначались буквами P (от нидерл. proberen — пытаться) и V (от нидерл. verhogen — поднимать выше) соответственно. Данные обозначения дал операциям над семафорами Дейкстра, но так как они не понятны для людей, говорящих на других языках, на практике обычно используются другие обозначения. Обозначения up и down впервые начали использоваться в языке Алгол 68[4].

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

Принято считать, что значение семафора является неотрицательным, но существует и другой подход к определению семафора, при котором под отрицательным значением понимается количество заблокированных задач с отрицательным знаком. При таком подходе уменьшение семафора является блокирующимся, если результат операции стал отрицательным[9].

Основным назначением семафора является разрешение или временный запрет на выполнение каких-либо действий, поэтому если значение счётчика семафора больше нуля, то говорят, что он находится в сигнальном состоянии, если же значение равно нулю — в несигнальном состоянии[10][⇨]. Уменьшение значения семафора также иногда называют захватом (англ. acquire[11]), а увеличение значения — отпусканием или освобождением (англ. release[11])[12], что позволяет сделать описание работы семафора более понятным в контексте контроля использования какого-либо ресурса или при использовании в критических секциях.

В общем виде семафор можно представить как объект, состоящий из:

  • переменной-счётчика, хранящей текущее значение семафора[13];
  • списка заблокированных в ожидании сигнального значения семафора задач[13];
  • функций атомарного увеличения и уменьшения значения семафора[13].

Концепция семафора хорошо подходит для синхронизации потоков, может использоваться для синхронизации процессов, однако совершенно не подходит для синхронизации взаимодействия компьютеров. Семафор является низкоуровневым примитивом синхронизации, поэтому, за исключением защиты критических секций, сам по себе может быть сложен в использовании[14].

Другим, более низкоуровневым, примитивом синхронизации является фьютекс. Он может предоставляться операционной системой и хорошо подходит для реализации семафоров на прикладном уровне при использовании атомарных операций над разделяемым счётчиком[15].

Алгоритмы использования[править | править код]

Типовые алгоритмы[править | править код]

Сигнализирование[править | править код]

Сигнализирование, также называемое уведомлением, является базовым назначением семафоров, оно гарантирует исполнение участка кода одной задачи после исполнения участка кода другой задачи[16]. Сигнальное использование семафора обычно предполагает установку его начального значения в 0, чтобы ожидающие сигнального состояния задачи могли блокироваться до наступления события. Сигнализирование выполняется через увеличение значения семафора, а ожидание — через уменьшение значения[6].

Пример сигнализирования семафором
Основной поток
  • Инициализировать семафор А (А ← 0)
Поток 1 Поток 2
  • Выполнить подготовку ресурса
  • Сигнализировать семафором А (А ← 1)

Разблокировка потока 2
  • Действия над общим ресурсом
Поток 2 первым получил процессорное время
  • Ожидать сигнального состояния А (блокировка)

Разблокировка, А ← 0
  • Действия над общим ресурсом

Семафоры хорошо подходят для сигнализирования одной или нескольким задачам, количество которых заранее известно. Если количество ожидающих сигнального состояния задач заранее неизвестно, то обычно используются условные переменные[⇨].

Взаимное исключение[править | править код]

В многопоточных приложениях часто требуется, чтобы отдельные участки кода, называемые критическими секциями, не могли работать параллельно, например, при доступе к какому-либо неразделяемому ресурсу или при изменении общих ячеек памяти. Для защиты таких участков можно использовать двоичный семафор или мьютекс[3]. Мьютекс является более безопасным в использовании, поскольку может быть отпущен только тем процессом или потоком, который его захватил[5]. Также использование мьютекса вместо семафора может быть более производительным за счёт оптимизации под два значения на уровне реализации ассемблерного кода.

Начальным значением семафора выставляется единица, означая, что он не захвачен — в критическую секцию пока никто не вошёл. Входом (англ. enter) в критическую секцию является захват семафора — его значение уменьшается до 0, что делает повторную попытку входа в критическую секцию блокирующейся. При выходе (англ. leave) из критической секции семафор отпускается, и его значения становится равным 1, разрешая снова входить в критическую секцию, в том числе и другим потокам или процессам[⇨].

Для разных ресурсов могут быть разные семафоры, отвечающие за критические секции. Таким образом, критические секции, защищённые разными семафорами, могут работать параллельно.

Пример работы критической секции на основе семафора
Основной поток
  • Инициализировать семафор А (А ← 1)
Поток 1 Поток 2
Поток 1 первым получил процессорное время

  • Захватить семафор А (А ← 0)
  • Выполнить действия над ресурсом
  • Отпустить семафор А (А ← 1)

Разблокировка потока 2
А захвачен в потоке 1

  • Захватить семафор А (блокировка)

Разблокировка, А ← 0
  • Выполнить действия над ресурсом
  • Отпустить семафор А (А ← 1)

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

Турникет[править | править код]

Частой бывает задача разрешения или запрета для одной или более задач прохождения через определённые контрольные точки. Для решения данной задачи используется алгоритм на основе двух семафоров, который по своей работе напоминает турникет, поскольку позволяет единовременно пропускать только одну задачу. Турникет основывается на семафоре, который в контрольных точках захватывается и сразу же освобождается. Если требуется закрыть турникет, то семафор необходимо захватить, в результате чего все задачи, проходящие через турникет будут блокироваться. Если требуется снова разрешить задачам проходить через турникет, что достаточно отпустить семафор, после чего задачи будут по очереди продолжать исполнение[18].

У поочерёдного прохождения через турникет есть большой недостаток — на каждое прохождение может происходить лишнее переключение контекста между задачами, в результате чего будет снижаться производительность алгоритма. В некоторых случаях решением может быть использование многоместного турникета, который разблокирует сразу несколько задач, что может осуществляться, например, циклическим отпусканием семафора, если используемая реализация семафора не поддерживает увеличение на произвольное число[19].

Турникеты на основе семафоров могут использоваться, например, в механизмах организации барьеров[20] или блокировок чтения и записи[21].

Выключатель[править | править код]

Ещё одним типовым алгоритмом на основе семафоров является реализация выключателя. Задачи могут захватывать выключатель и освобождать его. Первая задача, которая захватывает выключатель, включает его. А последняя задача, которая его освобождает, — выключает. Для данного алгоритма можно провести аналогию с выключателем света в комнате. Первый, кто входит в комнату, — включает свет, а последний, кто выходит, — выключает[22].

Алгоритм может реализовываться на основе счётчика захвативших выключатель задач и семафора выключателя, операции над которыми должны защищаться мьютексом. При захвате выключателя счётчик увеличивается на 1, а если его значение изменилось с нуля на один, то семафор выключателя захватывается, что равносильно включению выключателя. При этом увеличение счётчика вместе с проверкой и захватом семафора являются атомарной операцией, защищаемой мьютексом. При отпускании выключателя счётчик уменьшается, а если его значения стало нулевым, то семафор выключателя отпускается, то есть выключатель переводится в выключенное состояние. Уменьшение счётчика вместе с его проверкой и отпусканием семафора тоже должны быть атомарной операцией[22].

Алгоритм выключателя используется в более сложном механизме — блокировках чтения и записи[22][⇨].

Задача производителя и потребителя[править | править код]

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

Передача данных через кольцевой буфер[править | править код]

Кольцевой буфер представляет собой буфер с фиксированным количеством элементов, данные в который заносятся и обрабатываются в порядке очереди (FIFO). В однопоточном варианте исполнения для организации такого буфера достаточно 4-х ячеек памяти:

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

В многозадачной реализации алгоритм усложняется необходимостью синхронизации задач. Для случая двух задач (производитель и потребитель) можно ограничиться двумя ячейками памяти и двумя семафорами[8]:

  • индекс следующего элемента, доступного на чтение,
  • индекс следующего элемента, доступного на запись,
  • семафор, разрешающий чтение очередного элемента,
  • семафор, разрешающий запись очередного свободного элемента буфера.

Начальное значение семафора, отвечающего за чтение, устанавливается в 0, потому что очередь пуста. А значение семафора, отвечающего за запись, выставляется равным общему размеру буфера, то есть весь буфер доступен для заполнения. Перед заполнением очередного элемента в буфере семафор на запись уменьшается на 1, резервируя очередной элемент очереди для записи данных, после чего изменяется индекс на запись, а семафор на чтение увеличивается на 1, разрешая чтение добавленного в очередь элемента. Читающая задача, наоборот, захватывает семафор на чтение, после чего считывает очередной элемент из буфера и изменяет индекс следующего элемента на чтение, а затем отпускает семафор на запись, разрешая запись пишущей задаче в освободившийся элемент[8][⇨].

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

Передача данных через произвольный буфер[править | править код]

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

В механизмах синхронизации[править | править код]

Барьер[править | править код]

Барьер представляет собой механизм синхронизации критических точек у группы задач. Задачи могут пройти через барьер только все сразу. Перед входом в критические точки задачи группы должны блокироваться, пока входа в критическую точку не достигнет последняя задача из группы. Как только все задачи окажутся перед входом в свои критические точки, они должны продолжить своё исполнение[25].

Самое простое решение для организации барьера в случае двух задач основывается на двух бинарных семафорах А и Б, инициализируемых нулевым значением. В критической точке первой задачи необходимо перевести в сигнальное состояние семафор Б, а затем захватить семафор А. В критической точке второй задачи необходимо сначала перевести в сигнальное состояние семафор А, а затем — захватить Б. Какая бы задача не дошла до критической точки первой, она просигнализирует другой задаче, разрешив её исполнение. Как только обе задачи достигнут своих критических точек, их семафоры окажутся в сигнальном состоянии, что позволит им продолжить своё исполнение[26].

Подобная реализация — однопроходная, поскольку барьер не возвращается в исходное состояние, также у неё низкая производительность из-за использования одноместного турникета, что требует переключения контекста для каждой задачи, поэтому на практике данное решение малоприменимо[19].

Двухфазный барьер[править | править код]

Особенностью двухфазного барьера является то, что при его использовании каждая задача останавливается на барьере дважды — до критической точки и после. Два останова позволяют сделать барьер реентерабельным, поскольку второй останов позволяет вернуть барьер в изначальное состояние[27].

Универсальный реентерабельный алгоритм механизма двухфазного барьера может быть основан на использовании счётчика дошедших до критической точки задач и двух многоместных турникетов. Операции над счётчиком и управление турникетами должны быть защищены мьютексом. При этом должно быть заранее известно общее количество задач. Первый турникет пропускает задачи к критической точке и изначально должен быть заблокирован. Второй — пропускает задачи, которые только что прошли критическую точку, и изначально тоже должен быть заблокирован. Перед подходом к критической точке счётчик дошедших задач увеличивается на 1, а как только он достигает общего количества задач, то первый турникет разблокируется для всех задач, пропуская их к критической точке, что происходит атомарно через мьютекс вместе с увеличением счётчика и его проверкой. После критической точки, но до второго турникета, счётчик количества задач уменьшается на 1. По достижении нулевого значения второй турникет разблокируется для всех задач, при этом операции над вторым турникетом тоже происходят атомарно вместе с уменьшением счётчика и его проверкой. В результате все задачи останавливаются сначала перед критической точкой, а затем — после. После прохождения барьера состояния счётчика и турникетов оказываются в изначальных значениях[19].

Условная переменная[править | править код]

Условная переменная представляет собой способ оповещения ожидающих задач о возникновении какого-либо события[3]. Механизм условной переменной на прикладном уровне обычно строится на основе фьютекса и предусматривает функции ожидания события и отправки сигнала о его возникновении, но отдельные части этих функций должны защищаться мьютексом или семафором, поскольку помимо фьютекса в механизме условной переменной обычно присутствуют дополнительные разделяемые данные[28]. В простых реализациях фьютекс можно заменить семафором, который при оповещении необходимо будет отпустить столько раз, сколько задач подписалось на условную переменную, однако при большом количестве подписчиков оповещение может стать узким местом[29].

Механизм условной переменной предполагает наличие трёх операций: ожидание события, сигнализирование о событии одной задаче и оповещение всех задач о событии. Для реализации алгоритма на основе семафоров потребуются: мьютекс или двоичный семафор для защиты, собственно, самой условной переменной, счётчик количества ожидающих задач, мьютекс для защиты счётчика, семафор А для блокировки ожидающих задач и дополнительный семафор Б для своевременного пробуждения очередной ожидающей задачи[30].

При подписке на события счётчик подписавшихся задач атомарно увеличивается на 1, после чего отпускается предварительно захваченный мьютекс условной переменной. Затем захватывается семафор А для ожидания наступления события. По наступлению события сигнализирующая задача атомарно проверяет счётчик подписавшихся задач и оповещает очередную задачу о наступлении события, отпуская семафор А, а затем блокируется по семафору Б в ожидании подтверждения разблокировки. Получившая оповещение задача отпускает семафор Б и снова захватывает мьютекс условной переменной для возврата в изначальное состояние. Если же делается широковещательное оповещение всех подписанных задач, то семафор заблокированных задач А отпускается в цикле по количеству подписанных задач в счётчике. При этом оповещение происходит атомарно под защитой мьютекса счётчика, чтобы счётчик не мог измениться во время оповещения[30].

У решения на семафорах есть одна значимая проблема — два переключения контекста по сигнализированию, что сильно снижает производительность алгоритма, поэтому как минимум на уровне операционных систем оно обычно не применяется[30].

Интересным фактом является то, что сам семафор легко реализуется на основе условной переменной и мьютекса[15], а реализация условной переменной на основе семафоров — намного сложнее[30].

Блокировки чтения и записи[править | править код]

Одной из классических проблем является синхронизация доступа к ресурсу, доступному одновременно на чтение и на запись. Блокировки чтения и записи (англ.) призваны решить эту проблему и позволяют организовать раздельную блокировку ресурса на чтение и на запись, разрешая одновременное чтение, но запрещая одновременную запись. Запись также блокирует любое чтение[31]. Эффективный механизм может быть построен на базе фьютекса[15], однако блокировки чтения и записи могут быть также реализованы на основе комбинации мьютексов и семафоров или мьютексов и условной переменной.

Самым простым алгоритмом реализации на семафорах и мьютексах является использование выключателя бинарного семафора. Запись должна защищаться данным семафором. Первая читающая задача должна захватывать семафор с помощью выключателя, блокируя пишущие потоки, а последняя заканчивающая свою работы — должна отпускать семафор, разрешая пишущим задачам продолжить свою работу[22]. Однако данная реализация имеет одну серьёзную проблему, сравнимую с взаимной блокировкой, — ресурсное голодание[⇨] пишущих задач[32].

Универсальный алгоритм, лишённый описанной выше проблемы, включает в себя выключатель бинарного семафора А для организации критической секции читающих задач и турникет для блокировки новых читающих задач при наличии ожидающих пишущих. При появлении первой читающей задачи она захватывает семафор А с помощью выключателя, запрещая запись. Для пишущих задач семафор А защищает критическую секцию записи, поэтому, если он захвачен читающими задачами, все пишущие задачи блокируются при входе в свою критическую секцию. Однако захват пишущими задачами семафора А с последующей записью защищается семафором турникета. Поэтому, если произошла блокировка пишущей задачи из-за наличия читающих, турникет блокируется вместе с новыми читающими задачами. Как только последняя читающая заканчивает свою работу, семафор выключателя отпускается, и первая в очереди пишущая задача разблокируется. По окончании своей работы она отпускает семафор турникета, снова разрешая работу читающих задач[21].

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

В классических задачах[править | править код]

Обедающие философы[править | править код]

Одной из классических задач синхронизации является задача об обедающих философах. Задача включает в себя 5 обедающих за круглым столом философов, 5 тарелок, 5 вилок и общее блюдо с макаронами посреди стола. Перед каждым философом есть тарелка, а справа и слева — по одной вилке, но каждая вилка является общей между двумя соседними философами, а есть макароны можно только двумя вилками одновременно. При этом каждый из философов может или думать, или есть макароны[34].

Философами представлены взаимодействующие в программе потоки, а решение задачи включает в себя ряд условий[34]:

  • между философами не должно быть взаимоблокировок[⇨];
  • ни один философ не должен голодать, ожидая освобождение вилки[⇨];
  • должно быть возможным, чтобы одновременно ели хотя бы два философа.

Для решения задачи каждой вилке можно назначить двоичный семафор. Когда философ пытается взять вилку, семафор захватывается, а как только он заканчивает есть, семафоры вилок отпускаются. Проблема заключается в том, что вилку уже мог взять сосед, тогда философ блокируется до тех пор, пока его сосед не поест. Если одновременно все философы начнут приём пищи, то возможна взаимоблокировка[34].

Решением взаимоблокировки может быть ограничение количества одновременно обедающих философов до 4-х. В таком случае по крайней мере один философ сможет обедать, пока остальные ожидают. Ограничение можно реализовать через семафор с начальным значением 4. Каждый из философов будет захватывать данный семафор перед тем как взять вилки, а после приёма пищи — отпускать. Также данное решение гарантирует отсутствие голодания у философов, поскольку, если философ ожидает освобождения вилки соседом, тот рано или поздно её отпустит[34].

Существует и более простое решение. Взаимоблокировка возможна, если одновременно 5 философов держат вилку в одной и той же руке, например, если они все правши и вначале взяли правую вилку. Если же один из философов является левшой и берёт вначале левую вилку, то невозможны ни взаимоблокировка, ни голодание. Таким образом, достаточно, чтобы у одного из философов сначала захватывался семафор левой вилки, а затем — правой, в то время как у остальных философов — наоборот[34].

Американские горки[править | править код]

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

Состав вагонеток вместе с пассажирами можно представить как взаимодействующие задачи. Каждый пассажир должен блокироваться в ожидании своей очереди, а сам состав должен блокироваться на этапах заполнения и освобождения мест. Для загрузки и выгрузки состава можно воспользоваться двумя семафорами с выключателями, защищёнными каждый своим мьютексом, а для блокирования пассажиров на загрузку и на выгрузку можно использовать два семафора, отвечающие за места в вагонетках. Ожидающие пассажиры захватывают семафор на загрузку, а состав семафором на загрузку оповещает M из них о наличии свободных мест. Затем состав блокируется по выключателю, пока последний усаживающийся пассажир не просигнализирует соответствующим семафором, после чего начинается поездка. Перед поездкой пассажиры блокируются по семафору на выгрузку, что не даёт им выйти из состава. После поездки состав оповещает M пассажиров семафором на выгрузку, разрешая им выйти, а затем блокируется по семафору выключателя на выгрузку, ожидая, пока все пассажиры не выйдут. Как только последний пассажир выйдет из состава, он просигнализирует семафором второго выключателя и разрешит составу снова набирать пассажиров[35].

Проблемы использования[править | править код]

Ограничения семафоров[править | править код]

Концепция семафора предусматривает лишь операции уменьшения и увеличения на 1. При этом задача, уменьшающая семафор, обычно не может узнать, будет ли она заблокирована по нему или нет. При сигнализировании же нет возможности узнать, есть ли заблокированные по семафору задачи, а если задача сигнализирует семафором другой, заблокированной, — то обе задачи продолжают работать параллельно и нет никакой возможности узнать, какая из них получит процессорное время следующей[9].

Несмотря на ограничения концепции семафоров, конкретные их реализации могут быть лишены тех или иных ограничений. Например, возможность увеличения значения семафора на произвольное число предусмотрена в реализациях Linux[36], Windows[29] и System V (POSIX)[37]. А семафоры POSIX позволяют определить, будет ли блокировка по захвату семафора[38].

Сильные и слабые семафоры[править | править код]

Помимо ограничений самой концепции семафора, существуют и ограничения, накладываемые операционной системой или конкретной реализацией семафора. За распределение процессорного времени между процессами и потоками обычно отвечает планировщик задач операционной системы. Использование семафоров предъявляет ряд требований к планировщику и самой реализации семафоров для предотвращения ресурсного голодания, которое недопустимо в многозадачных приложениях[39].

  1. Если есть хотя бы одна задача, готовая к исполнению, она должна исполняться[39].
  2. Если задача готова к исполнению, время до начала её исполнения должно быть конечным[39].
  3. Если происходит сигнализирование семафором, по которому есть заблокированные задачи, то, по крайней мере, одна из них должна перейти в состояние готовности к исполнению[39].
  4. Если задача заблокирована по семафору, то количество других задач, которые будут разблокированы по тому же семафору до заданной, должно быть конечным[39].

Первые два требования необходимы, чтобы любая задача могла получить процессорное время и не находилась бесконечно в состоянии готовности, что уже позволяет писать приложения без ресурсного голодания. Третье требование необходимо для предотвращения ресурсного голодания при взаимном исключении, построенном на семафорах. Если сигнализирование будет лишь увеличивать счётчик семафора, но не будет пробуждать заблокированную по нему задачу, то возможна ситуация, когда одна и та же задача бесконечно отпускает и захватывает семафор, а другие заблокированные задачи не успевают перейти в состояние готовности, либо переходят, но гораздо реже. Однако даже при соблюдении третьего требования в случае большого количества заблокированных задач возможно ресурсное голодание, если каждый раз разблокируются одни и те же задачи. Данную проблему решает четвёртое требование, которое соблюдается, например, если заблокированные по семафору задачи пробуждаются в порядке очереди[39].

Соблюдение первых трёх требований позволяет реализовать так называемые слабые семафоры, а соблюдение всех четырёх — сильные[39].

Взаимные блокировки[править | править код]

При неправильном использовании семафоров могут возникать взаимные блокировки — ситуации, когда две или более параллельных задач оказываются заблокированными, ожидая отпускания блокировки друг от друга[40]. В такой ситуации задачи не смогут нормально продолжить своё исполнение и обычно один или более процессов требуется завершить принудительно. Взаимные блокировки могут быть как результатом простых ошибок работы с семафорами или другими способами синхронизации, так и вследствие состояния гонки, которое является более сложным в отладке.

Типовой ошибкой является вызов внутри критической секции подпрограммы, использующей ту же самую критическую секцию[41].

Наглядным примером взаимной блокировки могут служить вложенные друг в друга захваты бинарных семафоров А и Б, защищающих разные ресурсы, при условии обратного порядка их захвата в одном из потоков, что может быть обусловлено, например, стилевыми отличиями в написании кода программы. Ошибкой подобной реализации является состояние гонки, из-за которого программа может работать большую часть времени, но в случае параллельного захвата ресурсов высоки шансы на взаимную блокировку[42].

Пример взаимной блокировки с обратной вложенностью критических секций[42]
Основной поток
  • Инициализировать семафор А (А ← 1)
  • Инициализировать семафор Б (Б ← 1)
Поток 1 Поток 2
  • Захватить семафор А (А ← 0)

Б захвачен в потоке 2
  • Захватить семафор Б (блокировка)
  • Выполнить действия над ресурсом
  • Отпустить семафор Б
  • Отпустить семафор А
  • Захватить семафор Б (Б ← 0)

А захвачен в потоке 1
  • Захватить семафор А (блокировка)
  • Выполнить действия над ресурсом
  • Отпустить семафор А
  • Отпустить семафор Б

Ресурсное голодание[править | править код]

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

Типичным случаем для данной проблемы является простая реализация блокировок чтения и записи[⇦], при которой происходит запрет ресурса на запись при осуществлении чтения. Периодическое появление новых читающих задач может привести к неограниченной блокировке ресурса на запись. При слабой нагрузке на систему проблема может не проявляться длительное время, однако при высокой нагрузке может возникнуть ситуация, когда в каждый момент времени есть по крайней мере одна читающая задача, что сделает блокировку на запись постоянной на время высокой нагрузки[32]. При наличии семафора, отпускаемого при опустении очереди читающих задач, простым решением может быть добавление двоичного семафора (или мьютекса) для защиты кода пишущих задач, который в то же время будет выступать в роли турникета[⇦] для читающих задач. Пишущие задачи будут входить в критическую секцию и захватывать семафор пустой очереди, блокируясь по двум семафорам, пока есть читающие задачи. Читающие задачи будут блокироваться при входе в турникет, если пишущая задача ожидает окончания работы читающих. Как только последняя читающая задача закончит свою работу, она отпустит семафор пустой очереди, разблокировав ожидающую пишущую задачу[21].

Также ресурсному голоданию может быть подвержено даже взаимное исключение, если его реализация будет основана на слабых семафорах, однако существуют алгоритмы, позволяющие обходить ограничения слабых семафоров в данном случае[39].

Инверсия приоритетов[править | править код]

Другой проблемой может быть инверсия приоритетов, которая может проявиться при использовании семафоров процессами реального времени. Процессы реального времени могут быть прерваны операционной системой только для исполнения процессов с бо́льшим приоритетом. В этом случае процесс может заблокироваться по семафору в ожидании его отпускания процессом с меньшим приоритетом. Если в это время будет работать процесс со средним между двумя процессами приоритетом, то процесс с высоким приоритетом может оказаться заблокированным не неограниченный промежуток времени[43].

Проблема инверсии приоритетов решается наследованием приоритетов[44]. По возможности семафоры могут быть заменены на мьютексы, поскольку у мьютексов наследование приоритетов может быть заранее предусмотрено. Таким образом, при захвате мьютекса потоком с бо́льшим приоритетом произойдёт упреждающее повышение приоритета у задачи, владеющей мьютексом, для его скорейшего отпускания[7].

Повсеместное наследование приоритетов является крайне сложной в реализации задачей, поэтому поддерживающие её системы могут иметь лишь частичную реализацию. Также наследование приоритетов создаёт другие проблемы, например, к невозможности совмещения кода с наследованием приоритетов с кодом без наследования при использовании одной и той же критической секции[45].

При необходимости использования семафоров или при отсутствии поддержки наследования приоритетов алгоритмы могут модифицироваться для самостоятельного повышения приоритетов задачами[45].

Прикладное программирование[править | править код]

Семафоры в POSIX[править | править код]

Стандарты POSIX на уровне операционных систем предоставляют API языка Си для работы с семафорами как на уровне потоков, так и на уровне процессов через разделяемую память. Стандарты определяют тип данных семафора sem_t и набор функций для работы с ним[46]. Семафоры POSIX доступны в Linux, macOS, FreeBSD и других POSIX-совместимых операционных системах.

Функции для работы с семафорами POSIX из заголовочного файла semaphore.h[46]
Функция Описание
sem_init() Инициализация семафора с заданием начального значения счётчика и флага использования на уровне процессов
sem_destroy() Освобождение семафора
sem_open() Создание нового или открытие существующего именованного семафора
sem_close() Закрытие семафора после окончания работы с ним
sem_unlink() Удаление имени у именованного семафора (не уничтожает его)
sem_wait() Уменьшение значения семафора на 1
sem_timedwait() Уменьшение значения семафора на 1 с ограничением максимального времени блокировки, по истечении которого возвращается ошибка
sem_trywait() Попытка уменьшения значения семафора в неблокирующемся режиме, возвращает ошибку, если уменьшение без блокировки невозможно
sem_post() Увеличение значения семафора на 1
sem_getvalue() Получение текущего значения семафора

Одним из недостатков семафоров POSIX является способствующая ошибкам спецификация функции sem_timedwait(), которая оперирует часами реального времени (CLOCK_REALTIME)[47] вместо времени непрерывной работы системы (CLOCK_MONOTONIC), что может приводить к сбоям в работе программ при изменении системного времени и может оказаться критичным для встраиваемых устройств[48], но некоторые операционные системы реального времени предлагают аналоги данной функции, работающие с временем непрерывной работы системы[49]. Другим недостатком является отсутствие поддержки ожидания одновременно нескольких семафоров или семафора и файлового дескриптора.

В Linux семафоры POSIX реализованы в библиотеке Glibc на основе фьютекса[50].

Семафоры System V[править | править код]

Стандарты POSIX также определяют набор функций из стандарта X/Open System Interfaces (XSI) для межпроцессовой работы с семафорами в рамках операционной системы[51]. В отличие от обычных семафоров семафоры XSI можно увеличивать и уменьшать на произвольное число, они выделяются массивами, и их время жизни распространяется не на процессы, а на операционную систему. Таким образом, если забыть закрыть семафор XSI по завершению всех процессов приложения, то он продолжит существовать в операционной системе, что называется утечкой ресурса. В сравнении с семафорами XSI обычные семафоры POSIX намного проще в использовании, и у них может быть выше быстродействие[52].

Наборы семафоров XSI в рамках системы идентифицируются по числовому ключу типа key_t, однако можно создавать анонимные наборы семафоров для использования в рамках приложения, если указывать константу IPC_PRIVATE вместо числового ключа[53].

Функции для работы с семафорами XSI из заголовочного файла sys/sem.h
Функция Описание
semget() Создаёт или получает идентификатор набора семафоров с заданным числовым ключом[53]
semop() Выполняет атомарные операции уменьшения и увеличения на заданное число счётчика семафора по его номеру из набора с заданным идентификатором, а также позволяет заблокироваться в ожидании нулевого значения счётчика семафора, если в качестве заданного числа указан 0[37]
semctl() Позволяет управлять семафором по его номеру из набора с заданным идентификатором, в том числе получать и устанавливать текущее значение счётчика; также отвечает за уничтожение набора семафоров[54]

Семафоры в Linux[править | править код]

Операционные системы семейства Linux поддерживают семафоры POSIX, но также предлагают альтернативу семафорам в виде счётчика, привязанного к файловому дескриптору через системный вызов eventfd() с флагом EFD_SEMAPHORE. При чтении такого счётчика через функцию read() он уменьшается на 1, если его значение было ненулевым. Если же значение было нулевым, то происходит блокировка (если не указан флаг EFD_NONBLOCK), как и в случае с обычными семафорами. Функция write() увеличивает значение счётчика на число, которое записывается по файловому дескриптору. Преимуществом такого семафора является возможность ожидания сигнального состояния семафора наряду с другими событиями с помощью системных вызовов select() или poll()[36].

Семафоры в Windows[править | править код]

Ядро Windows также предоставляет API языка Си для работы с семафорами. Потоки, заблокированные по семафору, выстраиваются в очередь FIFO, но могут перейти в конец очереди в случае прерывания потока для обработки других событий[10].

Основные функции для работы с семафорами Windows API
Функция Описание
CreateSemaphoreA() Создание семафора с указанием начального значения счётчика, максимального значения и имени семафора
OpenSemaphoreW() Получение доступа к семафору по его имени, если он уже существует
CloseHandle() Закрытие семафора после окончания работы с ним
WaitForSingleObject() или WaitForMultipleObjects() Уменьшение значения семафора на 1 с блокировкой в случае нулевого значения счётчика; позволяет ограничивать максимальное время блокировки
ReleaseSemaphore() Увеличение значения семафора на указанную величину

Особенностями семафоров под Windows является возможность увеличивать семафор на произвольное число[29] и возможность ожидания его сигнального состояния вместе с блокирующим ожиданием других семафоров или объектов[55].

Поддержка в языках программирования[править | править код]

Семафоры обычно не поддерживаются на уровне языка программирования в явном виде, но часто предоставляются встроенными или сторонними библиотеками. В некоторых языках, таких как Ada[56] и Go[57], семафоры легко реализуются средствами языка.

Семафоры в распространённых языках программирования
Язык Модуль или библиотека Тип данных
Си pthread, rt sem_t
Ada GNAT.Semaphores Counting_Semaphore, Binary_Semaphore
C++ Boost boost::interprocess::interprocess_semaphore
C# System.Threading Semaphore
D core.sync.semaphore Semaphore
Go golang.org/x/sync/semaphore Weighted
Java java.util.concurrent java.util.concurrent.Semaphore
Python asyncio asyncio.Semaphore

Примеры использования[править | править код]

Защита критической секции[править | править код]

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

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

Пример синхронизации кольцевого буфера[править | править код]

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

Детали реализации[править | править код]

В операционных системах[править | править код]

В общем виде операционные системы осуществляют атомарные операции чтения и записи значения счётчика семафора, но детали реализации могут различаться на разных архитектурах. При захвате семафора операционная система должна атомарно уменьшить значение счётчика, после чего процесс может продолжить свою работу. Если же в результате уменьшения счётчика значение может стать отрицательным, то операционная система должна приостановить выполнение процесса до тех пор, пока значение счётчика не станет таким, чтобы операция уменьшения привела к неотрицательному результату[4]. При этом в зависимости от архитектуры на уровне реализации может быть выполнена как попытка уменьшения значения семафора[58], так и его уменьшение с получением отрицательного результата[13]. На уровне прикладного интерфейса обычно условно считается, что минимальным значением семафора является 0[3]. При увеличении значения семафора, по которому были заблокированы процессы, происходит разблокировка очередного процесса, а значение семафора на прикладном уровне остаётся равным нулю.

Блокировка на уровне операционной системы обычно не предполагает физического ожидания на процессоре, а передаёт управление процессором другой задаче, в то время как ожидающая отпускания семафора — попадает в очередь заблокированных по данному семафору задач[13]. В случае же, если количество готовых к исполнению задач меньше количества процессоров, ядро операционной системы может перевести свободные процессоры в режим экономии энергопотребления до наступления каких-либо событий.

На уровне процессоров[править | править код]

В архитектурах x86 и x86_64[править | править код]

Для синхронизации работы процессоров в многопроцессорных системах существуют специальные инструкции, позволяющие защитить доступ к какой-либо ячейке. В архитектуру x86 компанией Intel для ряда инструкций процессора предусмотрен префикс LOCK, позволяющий выполнять атомарные операции над ячейками памяти. Операции над ячейкой, выполняемые с префиксом LOCK, блокируют доступ остальных процессоров к ячейке, что на примитивном уровне позволяет организовывать легковесные семафоры с активным циклом ожидания[59].

Атомарное уменьшение значения семафора на 1 может быть выполнено при помощи инструкции DECL с префиксом LOCK, которая выставляет флаг знака CS в случае, если результирующее значение оказывается меньше нуля. Особенностью такого подхода является то, что значение семафора может оказываться меньше нуля, поэтому после уменьшения счётчика флаг CS может проверяться с помощью инструкции JNS, и, если знак отрицательный, то операционная система может заблокировать текущую задачу[60].

Для атомарного увеличения значения семафора на 1 может использоваться инструкция LOCK INCL. Если результирующее значение оказывается отрицательным либо равным нулю, то это означает наличие ожидающих задач, в таком случае операционная система может разблокировать очередную задачу. Для пропуска разблокировки процессов может использоваться инструкция JG, которая осуществляет переход к метке, если флаги нулевого результата операции (ZF) и знака результата (SF) сброшены в 0, то есть если значение больше 0[60].

Во время блокировки в случаях отсутствия текущих задач может использоваться инструкция HLT, предназначенная для перевода процессора в режим низкого энергопотребления с ожиданием прерываний[61], которые необходимо предварительно разрешать с помощью инструкции STI. Однако в современных процессорах более оптимальным может быть использование инструкций MWAIT и MONITOR. Инструкция MWAIT аналогична HLT, но позволяет пробудить процессор по записи в ячейку памяти по адресу, указанному в MONITOR. NWAIT можно использовать для мониторинга изменения ячейки семафора, однако в многозадачных операционных системах эта инструкция используется для мониторинга флага необходимости запустить планировщик задач на заданном ядре[62].

Снижение энергопотребления во время активного цикла ожидания может достигаться с помощью инструкции PAUSE[60].

В архитектуре ARM[править | править код]

В архитектуре ARMv7 для синхронизации памяти используются так называемые локальный и глобальный эксклюзивные мониторы, представляющие собой автоматы состояний, контролирующие атомарный доступ к ячейкам памяти. Атомарное чтение ячейки памяти может осуществляться с помощью инструкции LDREX, а атомарная запись — через инструкцию STREX, которая также возвращает флаг успеха операции[58].

Для уменьшения значения семафора необходимо дождаться, пока его счётчик не станет больше нуля. Ожидание может быть реализовано разными способами:

  • цикл активного ожидания в случае легковесного семафора, при котором периодически считывается значение счётчика с помощью инструкции LDREX[58];
  • блокировка с переводом процессора в энергосберегающий режим ожидания с помощью инструкций ожидания прерывания WFI или ожидания события WFE[58];
  • переключение контекста на исполнение другой задачи вместо блокировки процессора[58].

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

Увеличение значения семафора может представлять собой циклическое чтение текущего значения счётчика через инструкцию LDREX с последующим увеличением копии значения и попыткой записи обратно в ячейку счётчика с помощью инструкции STREX. При этом чтение может не являться цикличным в случае бинарного семафора, поскольку его увеличение всегда будет приводить к записи в счётчик одного и того же значения, а сама запись не обязана быть синхронизируемой. После успешной записи счётчика, если его изначальное значение было нулевым, требуется возобновить исполнение заблокированных задач, что в случае переключения контекста может решаться средствами операционных систем. Если процессор был заблокирован с помощью инструкции WFE, разблокировать его можно через инструкцию SEV, оповещающей о наличии какого-либо события. Также эта команда может использоваться для разблокировки процессора, если не используется переключение контекста[58].

После уменьшения или увеличения значения семафора выполняется инструкция DMB, обеспечивающую гарантию целостности памяти защищаемого семафором ресурса[58].

См. также[править | править код]

Примечания[править | править код]

  1. 1 2 The Open Group. 4. General Concepts. 4.17 Semaphore (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org. Дата обращения 12 июня 2019.
  2. Ching-Kuang Shene. Basic Concept (англ.). Multithreaded Programming with ThreadMentor: A Tutorial. Michigan Technological University. Дата обращения 7 июня 2019.
  3. 1 2 3 4 5 6 Камерон Хьюз, Трейси Хьюз. Параллельное и распределенное программирование с использованием С++. — Издательский дом Вильямс. — С. 194. — 667 с. — ISBN 9785845906861.
  4. 1 2 3 4 5 6 7 Эндрю С. Таненбаум. Современные операционные системы, 3-е издание. — P. 162–165.
  5. 1 2 pthread_mutex_unlock(3): lock/unlock mutex – Linux man page (англ.). linux.die.net. Дата обращения 1 мая 2019.
  6. 1 2 Ching-Kuang Shene. Three Commonly Used Techniques (англ.). Multithreaded Programming with ThreadMentor: A Tutorial. Michigan Technological University. Дата обращения 7 июня 2019.
  7. 1 2 The Open Group. pthread_mutexattr_setprotocol (англ.). The Single UNIX ® Specification, Version 2. pubs.opengroup.org (1997). Дата обращения 9 июня 2019.
  8. 1 2 3 Эндрю С. Таненбаум, Т. Остин. Архитектура компьютера = Structured Computer Organization. — 5-е издание. — СПб: Питер, 2010. — С. 510–516. — 844 с. — ISBN 9785469012740.
  9. 1 2 3 Allen B. Downey, 2.1 Definition, с. 7—8.
  10. 1 2 Побегайло А. П. Системное программирование в Windows. — СПб: БХВ-Петербург, 2006. — С. 137–142. — 1056 с. — ISBN 9785941577927.
  11. 1 2 Java API Reference (англ.). docs.oracle.com. Дата обращения 4 мая 2019.
  12. Олег Цилюрик. Инструменты программирования в ядре: Часть 73. Параллелизм и синхронизация. Блокировки. Часть 1. www.ibm.com (13 августа 2013). Дата обращения 4 мая 2019.
  13. 1 2 3 4 5 Daniel Pierre Bovet, Marco Cesati. Understanding the Linux Kernel. — P. 23—25.
  14. Эндрю С. Таненбаум. Современные операционные системы, 3-е издание. — P. 176.
  15. 1 2 3 Rémi Denis-Courmont. Other uses of futex (англ.). Remlab. Remlab.net (21 September 2016). Дата обращения 15 июня 2019.
  16. Allen B. Downey, 3.1 Signaling, с. 11—12.
  17. Эндрю С. Таненбаум. Современные операционные системы, 3-е издание. — P. 170—176.
  18. Allen B. Downey, 3.6.4 Barrier solution, с. 29.
  19. 1 2 3 Allen B. Downey, 3.7.6 Preloaded turnstile, с. 43.
  20. Allen B. Downey, 3.5.4 Barrier solution, с. 29.
  21. 1 2 3 Allen B. Downey, 4.2.5 No-starve readers-writers solution, с. 75.
  22. 1 2 3 4 Allen B. Downey, 4.2.2 Readers-writers solution, с. 69—71.
  23. C.-K. Shene. ThreadMentor: The Producer/Consumer (or Bounded-Buffer) Problem (англ.). Multithreaded Programming with ThreadMentor: A Tutorial. Michigan Technological University. Дата обращения 1 июля 2019.
  24. Allen B. Downey, 4.1.2 Producer-consumer solution, с. 59—60.
  25. Allen B. Downey, 3.6 Barrier, с. 21—22.
  26. Allen B. Downey, 3.3.2 Rendezvous solution, с. 15.
  27. Allen B. Downey, 3.7.5 Reusable barrier solution, с. 41—42.
  28. Rémi Denis-Courmont. Condition variable with futex (англ.). Remlab. Remlab.net (21 September 2016). Дата обращения 16 июня 2019.
  29. 1 2 3 Microsoft. ReleaseSemaphore function (synchapi.h) (англ.). docs.microsoft.com. Дата обращения 5 мая 2019.
  30. 1 2 3 4 Andrew D. Birrell. Implementing Condition Variables with Semaphores (англ.). Microsoft Research. www.microsoft.com (January 2003).
  31. Allen B. Downey, 4.2 Readers-writers problem, с. 65—66.
  32. 1 2 3 Allen B. Downey, 4.2.3 Starvation, с. 71.
  33. Олег Цилюрик. Инструменты программирования в ядре: Часть 73. Параллелизм и синхронизация. Блокировки. Часть 1. www.ibm.com (13 августа 2013). Дата обращения 12 июня 2019.
  34. 1 2 3 4 5 Allen B. Downey, 4.4 Dining philosophers, с. 87—88.
  35. 1 2 Allen B. Downey, 5.8 The roller coaster problem, с. 153.
  36. 1 2 eventfd(2) - Linux manual page (англ.). man7.org. Дата обращения 8 июня 2019.
  37. 1 2 semop (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org. Дата обращения 12 июня 2019.
  38. IEEE, The Open Group. sem_trywait (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org (2008). Дата обращения 29 июня 2019.
  39. 1 2 3 4 5 6 7 8 Allen B. Downey, 4.3 No-starve mutex, с. 81—82.
  40. Эндрю С. Таненбаум. Современные операционные системы, 3-е издание. — P. 511—512.
  41. Rohit Chandra, Leo Dagum, David Kohr, Ramesh Menon, Dror Maydan. Parallel Programming in OpenMP  (англ.). — Morgan Kaufmann, 2001. — С. 151. — 250 с. — ISBN 9781558606715.
  42. 1 2 Эндрю С. Таненбаум. Современные операционные системы, 3-е издание. — P. 510–511.
  43. sem_wait (англ.). The Single UNIX ® Specification, Version 2. pubs.opengroup.org (1997). Дата обращения 9 июня 2019.
  44. Priority inversion - priority inheritance (англ.). The Linux Foundation Wiki. wiki.linuxfoundation.org. Дата обращения 9 июня 2019.
  45. 1 2 Victor Yodaiken. Against priority inheritance (англ.). Against priority inheritance. Finite State Machine Labs (23 September 2004).
  46. 1 2 IEEE, The Open Group. semaphore.h - semaphores (англ.). The Open Group Base Specifications Issue 7, 2018 edition. pubs.opengroup.org. Дата обращения 8 июня 2019.
  47. sem_timedwait.3p - Linux manual page (англ.). man7.org. Дата обращения 5 мая 2019.
  48. 112521 – monotonic sem_timedwait (англ.). bugzilla.kernel.org. Дата обращения 5 мая 2019.
  49. sem_timedwait(), sem_timedwait_monotonic() (англ.). QNX Neutrino Realtime Operating System. www.qnx.com. Дата обращения 5 мая 2019.
  50. futex(2) - Linux manual page (англ.). man7.org. — Раздел «NOTES». Дата обращения 23 июня 2019.
  51. The Open Group. 2. General Information. 2.7 XSI Interprocess Communication (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org. Дата обращения 11 июня 2019.
  52. Vikram Shukla. Semaphores in Linux (англ.) (2007-24-05). — Оригинальная статья есть на web.archive.org, но в неполном виде.
  53. 1 2 semget (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org. Дата обращения 12 июня 2019.
  54. semctl (англ.). The Open Group Base Specifications Issue 7. pubs.opengroup.org. Дата обращения 12 июня 2019.
  55. Microsoft. WaitForMultipleObjects function (synchapi.h) (англ.). docs.microsoft.com. Дата обращения 5 мая 2019.
  56. M. Ben-Ari, Môtî Ben-Arî. Principles of Concurrent and Distributed Programming  (англ.). — Addison-Wesley, 2006. — С. 132. — 388 с. — ISBN 9780321312839.
  57. Semaphores - Go Language Patterns (англ.). www.golangpatterns.info. Дата обращения 8 июня 2019.
  58. 1 2 3 4 5 6 7 ARM. ARM Synchronization Primitives.
  59. Руслан Аблязов. Программирование на ассемблере на платформе x86-64. — Litres, 2017-09-05. — С. 273—275. — 304 с. — ISBN 9785040349203.
  60. 1 2 3 Daniel Pierre Bovet, Marco Cesati. Understanding the Linux Kernel. — P. 202.
  61. The Linux BootPrompt-HowTo: General Non-Device Specific Boot Args (англ.). www.tldp.org. Дата обращения 3 мая 2019.
  62. Corey Gough, Ian Steiner, Winston Saunders. Energy Efficient Servers: Blueprints for Data Center Optimization. — Apress, 2015. — С. 175. — 347 с. — ISBN 9781430266389.

Литература[править | править код]