Примечание. Я сильно отредактировал этот вопрос для ясности после того, как устроил ему публичный мозговой штурм. Однако описанные фактические алгоритмы и вопрос о том, достаточно ли их для предотвращения гонок, должны быть идентичными.
Я пытаюсь реализовать семафоры, которые позволяют избежать состояния гонки, описанного в ошибке glibc номер 12674:
http://sourceware.org/bugzilla/show_bug.cgi?id=12674
По сути, эффективный способ написать семафор на основе фьютекса, если вас не волнует это состояние гонки для уничтожения:
Почта:
- Значение семафора атомарного приращения.
- Изучите количество официантов. Если он ненулевой, выполните пробуждение фьютексом.
Ждать:
- Атомарный декремент при положительном значении семафора (и возврат в случае успеха).
- Если это не удается, увеличьте количество ожидающих и выполните ожидание фьютекса.
- При пробуждении уменьшите число ожидающих, выполните цикл и повторите попытку.
Шаг 2 почтовой операции - это то, что небезопасно.
Есть две очевидные реализации, которые позволяют избежать этой проблемы, но обе имеют серьезные проблемы:
Первое решение - вообще не хранить счетчик официантов или флаг и всегда выполнять пробуждение фьютексом по почте. Это, очевидно, безопасно, но сводит на нет всю цель фьютексов (сохранение несогласованного случая в пользовательском пространстве).
Второе решение - не хранить счетчик официантов, а вместо этого позволить значению семафора уменьшаться до -1 при ожидании. После этого пост-операция переходит от -1 к 1 и будит всех официантов. Один из них преуспевает в уменьшении значения семафора до 0, и если они остались, они устанавливают значение -1, так что следующий пост выполнит еще одно пробуждение. Это решение также очевидно безопасно, но приводит к панике потоков, борющихся за семафор при его публикации.
Таким образом, первое решение хорошо работает только для семафоров, которые всегда используются, а второе - только для семафоров, у которых обычно не более одного официанта. Ни то, ни другое неприемлемо для общего использования.
А теперь перейдем к поиску реального решения ...
Здесь следует отметить, что есть еще несколько требований, которые усложняют реальную реализацию. Пост-операция должна быть асинхронно-сигнально-безопасной (поэтому она в основном не может использовать блокировки), а операция ожидания требуется, чтобы разрешить прерывание сигналами, тайм-аутом или отменой потока. На практике это означает, что пост-операция должна иметь возможность безопасно «отменить» любые изменения, которые она вносит в состояние семафора. Я замалчивал такие проблемы, потому что мой подход, кажется, не имеет с ними проблем, но они делают некоторые очевидные изменения / решения невозможными, поэтому любой, предлагающий новые подходы в качестве ответа, должен знать об этих проблемах ... em>
У меня есть предлагаемое решение, но я не уверен, будет ли оно подвержено новым (и, возможно, худшим) условиям гонки. Я опишу его здесь и надеюсь, что некоторые боги параллелизма (или, по крайней мере, полубоги) проявят любезность проверить его на правильность.
Мой подход является чем-то вроде гибрида второго «плохого» решения, описанного выше, и оригинального подхода (с гонкой, описанной в отчете об ошибке glibc). Он использует как счетчик официантов, так и флаг официанта (-1 хранится в значении семафора).
Ключевым изменением операции ожидания является то, что она сохраняет -1 вместо 0 в значении семафора всякий раз, когда есть официанты (количество уже существующих официантов или себя как нового официанта).
Ждать:
- Прочитать значение семафора.
- Если значение положительное, определите новое значение семафора следующим образом: если значение равно 1 и есть ожидающие, новое значение должно быть -1; в противном случае просто уменьшите старое значение. Используйте функцию сравнения и замены, чтобы обновить значение, и верните успех, если это удастся.
- В противном случае увеличьте число ожидающих, атомарно замените значение 0 на -1 и выполните ожидание фьютекса с -1 в качестве значения.
- При пробуждении уменьшите число ожидающих, выполните цикл и повторите попытку.
Ключевым изменением операции post является то, что она выполняет чтение счетчика официантов до увеличения значения семафора, а не после:
Почта:
- Прочтите и сохраните значение семафора.
- Считайте и сохраните счетчик официантов.
- Определите новое значение семафора (-1 становится 1, иначе просто увеличивается).
- Атомарное сравнение и замена для обновления значения семафора. В случае неудачи перейти к 1.
- Если сохраненное количество официантов было ненулевым или значение семафора было -1, выполните пробуждение фьютекса.
Сравнение и замена на шаге 4 обеспечивает некоторую безопасность, что счетчик официантов все еще правильный, но все еще есть гонка ABA - между шагами 1 и 4 возможно, что другие потоки выполняют операции ожидания и публикации, которые оставляют значение семафора одинаковым как его начальное значение.
При поиске случаев, когда этот алгоритм может не разбудить официантов, нам нужно рассматривать только случаи, когда начальное считывание счетчика официантов равно 0, а считанное значение семафора не равно -1. Кроме того, если значение семафора положительное и нет уже существующих официантов, текущий пост не несет ответственности за какие-либо пробуждения, так что этот случай тоже не интересен. Нам осталось изучить случаи, когда операция ожидания начинается с нулевого значения семафора и нулевого счетчика ожидания. В этой ситуации, чтобы не иметь состояния гонки, любое событие, которое происходит между шагами 2 и 4, которое приводит к появлению новых ожидающих, должно изменить значение семафора, так что сравнение и замена на шаге 4 не удастся. Очевидно, что любой отдельный промежуточный пост или ожидание изменит значение семафора (на 1 или -1, соответственно), поэтому проблема, в частности, представляет собой последовательность операций, которые приводят к значению семафора, равному 0, но присутствию официантов.
Я считаю, что этого не может произойти из-за процедуры, выполненной в операции ожидания, но я не убедил себя на 100%.
Наконец, вот несколько примеров гонок, которые случаются, если вы ослабляете мой алгоритм, чтобы установить мотивацию того, что он делает, если это не ясно.
Ошибка 1: используется чистый счетчик ожидания, нет флага -1 в значении семафора. Банальная гонка выглядит так:
- Значение семафора начинается с 0
- Поток 1 начинает публикацию, считывает значение семафора 0 и счетчик ожидания 0.
- Поток 2 начинает ожидание, увеличивает счетчик ожидания и ожидает фьютекс.
- Поток 1 выполняет успешное сравнение и замену, возвращается, не разбудив официанта.
Ошибка 2: использование счетчика официантов и новые официанты устанавливают значение семафора на -1, но просто уменьшают значение семафора, когда ожидание завершается успешно (без установки его на -1, если другие потоки все еще ждут):
- Значение семафора начинается с 0
- Поток 1 начинает публикацию, считывает значение семафора 0 и счетчик ожидания 0.
- Потоки 2 и 3 ждут, увеличивая счетчик ожидания и ожидание фьютекса.
- Обсуждение 4 сообщений, установка значения семафора равным 1.
- Поток 2 пробуждает и уменьшает значение семафора до 0, счетчик официантов до 1.
- Поток 1 выполняет успешное сравнение и замену, возвращается без пробуждения потока 3.
mutex_destroy
; мне кажется, что такое поведение действительно должно быть разрешено, хотя я вижу, что это может быть немного неясно для случая, когда сам семафор синхронизирует свое уничтожение ... - person bdonlan   schedule 02.08.2011pthread_mutex_lock
. К счастью, эту проблему для мьютексов немного легче решить, поскольку недопустимо, чтобы несколько потоков одновременно вызывалиpthread_mutex_unlock
(тогда как для нескольких потоков разрешено отправлять семафор, поскольку семафоры не имеют владельцев). На самом деле исправление довольно близко к тому, что я описал для семафоров, но мне кажется, легче установить его правильность ... - person R.. GitHub STOP HELPING ICE   schedule 02.08.2011