Доступ к сепаратным объектам
Сейчас у нас уже достаточно сведений, чтобы предложить подходящие механизмы синхронизации параллельных ОО-систем.
Но, как мы уже отмечали, это не всегда работает - по крайней мере, до тех пор, пока не обеспечен безопасный исключающий доступ к buffer. Иначе между моментом, когда проверяется условие для count и моментом, когда выполняется первое удаление remove, любой другой клиент может придти и удалить элемент, так что эта программа аварийно завершится, пытаясь применить remove к пустой структуре.
В следующем примере предполагается, что компонент item, не имеющий побочного эффекта, возвращает элемент, удаляемый компонентом remove:
if not buffer.empty then value := buffer.item; buffer.remove endБез защиты буфера buffer другой клиент может добавить или удалить элемент в промежутке между вызовами item и remove. В один прекрасный день автор этого фрагмента получит доступ к одному элементу, а удалит другой, так что можно, например, (при повторении указанной схемы) получить доступ к одному и тому же элементу дважды! Все это очень плохо.
Сделав buffer аргументом вызывающей подпрограммы, мы устраняем эти проблемы: гарантируется, что buffer будет зарезервирован на все время выполнения вызова подпрограммы.
Конечно, вина за ошибки в рассмотренных примерах лежит на невнимательных разработчиках. Но без правила сепаратного вызова такие ошибки совершаются легко. По-настоящему плохо то, что поведение во время выполнения становится недетерминированным, поскольку оно зависит от относительной скорости клиентов. Из-за этого ошибка будет блуждающей, сейчас в одном месте программы, при следующем запуске - в другом. Еще хуже то, что она, вероятно, будет проявляться редко: во всяком случае (в первом примере) конкурирующий клиент должен оказаться очень удачливым, чтобы протиснуться между проверкой count и первым вызовом remove. Поэтому такую ошибку очень трудно повторить и изолировать.
Такие коварные ошибки ответственны за кошмарную репутацию отладки параллельных систем. Всякое правило, существенно уменьшающее вероятность их появления, оказывает большую помощь разработчикам.
Учитывая правило сепаратного вызова, наши примеры следует записать в виде следующих процедур, использующих сепаратный тип BOUNDED_BUFFER:
remove_two (buffer: BOUNDED_BUFFER) is -- Удаляет два самых старых элемента do if buffer.count >= 2 then buffer.remove; buffer.remove end end get_and_remove (buffer: BOUNDED_BUFFER) is -- Присваивает самый старый элемент value и удаляет его do if not buffer.empty then value := buffer.item; buffer.remove end endЭти процедуры могут быть частью некоторого класса приложения; в частности, они могут быть описаны в классе BUFFER_ACCESS (ДОСТУП_К_БУФЕРУ), инкапсулирующем операции работы с буфером и служащем родительским классом для различных видов буферов.
Обе эти процедуры взывают о предусловии. Вскоре мы позаботимся о нем.