Да, вы можете сделать это, если ваша операционная система предлагает подходящий механизм «парковки» и «отмены парковки», для которого не требуется блокировка для разблокировки. Под парком понимается разрешение перехода потока в спящий режим (блокировка ОС), а отмена - пробуждение этого потока.
Вы уже близки с вашим атомным счетчиком и подходом Кондвара. Проблема в том, что condvar a mutex требуется как часть семантики. Таким образом, вы должны отказаться от condvars и пойти немного ниже уровня. Во-первых, вы должны упаковать все состояния, такие как текущее значение семафора, есть ли какие-либо официанты (и, возможно, сколько их ожидают), в одно атомарное значение и манипулировать этим атомарно с помощью сравнения и обмена. Это предотвращает гонки, которые могли бы произойти, если бы вы использовали их как отдельные значения.
Затем вы можете нарисовать диаграмму состояний, показывающую все возможные состояния семафора с ребрами для всех возможных переходных состояний (например, состояние «нет официантов» перейдет в состояние «да официантов», когда прибудет официант). Вы реализуете все переходы с помощью сравнения и обмена, и всякий раз, когда он терпит неудачу, вы должны пересчитать переход, так как он мог измениться!
Тогда вам нужно только реализовать блокировку. В Windows вы должны использовать Events - автоматический или ручной сброс. У обоих есть свои преимущества и изюминки, и у этой кошки есть больше, чем один способ. Например, вы, вероятно, можете заставить его работать с одиночным общим событием и событиями автоматического сброса.
Однако, вот эскиз одного механизма, который использует объект официанта на поток в очереди без блокировки. Семафор состоит из управляющего слова с атомарной манипуляцией и списка без блокировки с типом элемента waiter_node
или стеком или любым другим готовым параллельным списком, который вы хотите использовать.
Предположим, что каждому потоку принадлежит объект waiter_node, который содержит только один объект Event сброса вручную. Это может быть создано один раз и сохранено в TLS (вероятно, наиболее эффективным) или выделено по требованию каждый раз, когда необходимо выполнить ожидание, и отменено распределение, когда ожидание выполнено.
Вот основная схема:
Подождите
- Если семафор доступен (положительный), CAS уменьшит его и продолжит.
- Если семафор недоступен (ноль), поток вызывает
ResetEvent
для своего waiter_node
, а затем помещает событие в список официантов, проверяет, что значение sem все еще равно нулю, а затем вызывает WaitForObject
для него. waiter_node
. Когда это вернется, запустите процедуру ожидания сверху.
Сообщение
- Увеличить контрольное слово. Введите
waiter_node
, если есть, и наберите SetEvent
.
Здесь есть всевозможные «расы», такие как waiter_node
, выполняемая операцией post
до того, как ожидающий поток даже спит на нем, но они должны быть доброкачественными.
Есть много вариантов, даже в этом дизайне на основе очереди официантов. Например, вы можете объединить список «заголовок» и контрольное слово, чтобы они были одним и тем же. Тогда wait
не нужно дважды проверять счетчик семафоров, поскольку операция push одновременно проверяет состояние семафора. Вы также можете реализовать «прямую передачу обслуживания», когда поток post
ing вообще не увеличивает управляющее слово, если есть официанты, а просто выводит его и пробуждает в нем информацию о том, что он успешно получил семафор.
В Linux вы заменяете Event
на futex
. Здесь проще реализовать решение «с одним фьютексом», поскольку futex
допускает атомарную операцию проверки и блокировки внутри ядра, которая позволяет избежать многих гонок, присущих решению Event
. Таким образом, базовый эскиз - это одно контрольное слово, и вы переходите атомарно с помощью CAS, а затем используете futex()
с FUTEX_WAIT
, чтобы выполнить вторую проверку контрольного слова и заблокировать атомарно (это атомная проверка-и-сон это сила futex
).