Примитивы синхронизации
Я слышу крик в темноте
Наверное, это сигнал.
В. Бутусов
Посмотрев на примеры 7.2 и 7.4, внимательный читатель должен отметить, что используемая конструкция подозрительно похожа на работу с внешними устройствами в режиме опроса. Действительно, опрос флаговой переменном в цикле хотя и обеспечивает гарантию взаимоисключения, но обладает всеми недостатками, которые мы указывали для опроса внешнего устройства.
В случае исполнения параллельных нитей на одном процессоре, данный метод имеет еще один недостаток: пока одна из нитей занимается опросом, никакая другая нить не может исполняться, потому что процессор загружен непродуктивной работой.
Легко видеть, что в данном случае использование прерываний или какого-то их аналога проблемы не решит: в лучшем случае, "прерывание" будет вызываться не в той нити, в которой нужно, сводя задачу взаимоисключения к предыдущей, только с уже новой флаговой переменной, а в худшем – приведет к возникновению еще одной нити. Задача взаимодействия между асинхронными нитями, таким образом, сводится к требованию того, чтобы нити в какие-то моменты переставали быть асинхронными, синхронизовались.
Если у нас уже есть примитив взаимного исключения, мы можем решить задачу синхронизации, предоставив еще один примитив, который позволяет активному процессу остановиться, ожидая, пока флаговая переменная не примет "правильное" значение, и продолжить исполнение после этого. При обработке прерываний роль такого примитива может исполнять команда остановки процессора: у всех современных процессоров прерывание останавливает "исполнение" этой команды, а возврат из обработчика передает управление на следующую команду, таким образом выводя процессор из спящего состояния.
В многопроцессорной конфигурации можно ввести средство, при помощи которого один процессор может вызывать прерывание другого – и тогда каждый из процессоров системы сможет ожидать другого, переходя в режим сна. При реализации же многопоточности на одном процессоре (см. разд. "Вытесняющая многозадачность") примитив засыпания (блокировки) нити должен предоставляться модулем, ответственным за переключение потоков.
Впрочем, если операции над флагом, засыпание потока и его пробуждение реализованы разными примитивами, мы рискуем получить новую проблему (пример 7.5). Она состоит в том, что если пробуждающий сигнал поступит в промежутке между операторами testandset и pause, мы его не получим. В результате операция pause приведет к засыпанию нашей нити навсегда.
Пример 7.5. Ошибка потерянного пробуждения (lost wake-up bug).
program пауза var flag: Boolean; procedure процесс1 var myflag: Boolean while True do begin myflag: = True; testandset(myflag, flag); if myflag then (* Обратите внимание, что проверка флага * * и засыпание – это разные операторы! *) pause; критическая секция(); flag: = False; end end;
Одно из решений состоит в усложнении примитива pause: он должен засыпать, если и только если сигнал еще не приходил. Усложнение получается значительное: мало того, что перед засыпанием надо проверять нетривиальное условие, необходимо еще предусмотреть какой-то способ сброса этого условия, если мы предполагаем многократное использование нашего примитива.
Если писать на ассемблере или родственных ему языках, можно пойти и более изощренным путем (пример 7.6). Подмена адреса возврата в обработчике прерывания гарантирует нам, что если прерывание по установке флага произойдет в промежутке между метками label и ok, мы перейдем на метку label и, вместо того, чтобы заснуть навеки, благополучно проверим флаг и войдем в критическую секцию.
Пример 7.6. Обход ошибки потерянного пробуждения.
.globl flag flag: db 0 jmpbuf: dw 0 proc flag_interrupt push eax tst jmpbuf bz setflagonly ; подменяем адрес возврата move eax, jmpbuf move sp[RETURN_ADDRESS_OFFSET], eax setflagonly move eax, 1 move flag, eax pop eax iret endp proc process1 inove eax, setjmp move jmpbuf, eax setjmp: move eax, 1 lock xchg eax, flag tst eax bz ok halt ok xor eax, eax move jmpbuf, eax критическая секция xor eax, eax move flag, eax endp
Более элегантный и приемлемый для языков высокого уровня путь решения этой проблемы состоит в том, чтобы объединить в атомарную операцию проверку флага и засыпание. Нас с читателем можно поздравить с изобретением двоичного семафора Дейкстры.