Рандеву: entry, accept и select

Рандеву — это синхронная встреча двух задач: одна вызывает точку входа, другая её принимает, и они на миг сливаются для совместного действия, после чего расходятся.

Рандеву (rendezvous) — механизм прямого взаимодействия задач в Ada: задача-сервер объявляет точку входа (entry) и принимает обращения (accept), а задача-клиент вызывает её; обе синхронизируются, обмениваются данными во время совместного действия и затем продолжают независимо.

Если задачи — это «кто исполняет», то рандеву — «как они договариваются». Само слово (фр. rendez-vous, «встреча») точно передаёт суть: две задачи назначают свидание в конкретной точке программы. Та, что готова раньше, ждёт другую; когда обе на месте — происходит синхронная встреча, во время которой выполняется согласованный код и передаются данные. Это высокоуровневая, безопасная альтернатива разделяемой памяти с ручными блокировками: вместо «общей переменной под мьютексом» — явный, структурированный диалог. Модель восходит к идеям взаимодействующих последовательных процессов (CSP) Хоара и была встроена в Ada как основной способ межзадачного общения.

Точки входа: entry, accept, вызов

Сервер объявляет точку входа в своей спецификации словом entry — синтаксически она похожа на процедуру и может иметь параметры (в том числе out для возврата данных клиенту). В теле сервер «открывает» приём оператором accept: когда исполнение доходит до accept, сервер ждёт клиента; когда клиент вызывает точку входа — оба синхронизируются, выполняется тело accept, после чего обе задачи расходятся.

with Ada.Text_IO; use Ada.Text_IO;
procedure Rendezvous_Demo is

   task Server is
      entry Deposit (Amount : in Integer);      -- точка входа с параметром
      entry Get_Total (Total : out Integer);    -- возврат данных клиенту
   end Server;

   task body Server is
      Sum : Integer := 0;
   begin
      loop
         select
            accept Deposit (Amount : in Integer) do
               Sum := Sum + Amount;             -- тело рандеву: клиент ждёт
            end Deposit;
         or
            accept Get_Total (Total : out Integer) do
               Total := Sum;                    -- отдаём сумму клиенту
            end Get_Total;
         or
            terminate;                          -- завершиться, если все ушли
         end select;
      end loop;
   end Server;

begin
   Server.Deposit (100);          -- вызов точки входа = клиентская сторона
   Server.Deposit (50);
   declare
      T : Integer;
   begin
      Server.Get_Total (T);
      Put_Line ("Итого:" & T'Image);   -- Итого: 150
   end;
end Rendezvous_Demo;

Вывод:

Итого: 150

Обратите внимание на симметрию: клиент пишет Server.Deposit (100); — почти как вызов процедуры, но на деле это запрос на встречу. Сервер в теле accept Deposit (...) do ... end; исполняет согласованный код, пока клиент ждёт. Данные текут в обе стороны через параметры точки входа: in — от клиента к серверу, out — обратно.

Оператор select: выбор и недетерминизм

Реальный сервер должен уметь принимать разные обращения и реагировать на ситуацию. Для этого служит оператор select — он перечисляет несколько альтернатив accept через or, и сервер принимает ту точку входа, по которой первым пришёл клиент. Если ждут сразу несколько — выбор недетерминирован (любой из готовых). Это элегантно решает задачу «обслуживать несколько видов запросов одной задачей».

select богат на формы. Альтернатива terminate; означает «если все потенциальные клиенты завершились и звать меня больше некому — заверши и меня»: это корректный способ остановить задачу-сервер без специального сигнала. Альтернатива delay внутри select задаёт тайм-аут ожидания. А сторожевые условия (guards) вида when Условие => accept ... позволяют открывать приём точки входа только при выполнении условия — основа классических задач синхронизации (буфер принимает Put, только если не полон).

--  Сторожевые условия: ограниченный буфер на рандеву
task body Bounded_Buffer is
   Buf   : array (1 .. Size) of Item;
   Count : Natural := 0;
   Head, Tail : Natural := 1;
begin
   loop
      select
         when Count < Size =>             -- принимать Put, только если есть место
            accept Put (X : in Item) do
               Buf (Tail) := X;
            end Put;
            Tail := Tail mod Size + 1;
            Count := Count + 1;
         or
            when Count > 0 =>             -- отдавать Get, только если есть данные
            accept Get (X : out Item) do
               X := Buf (Head);
            end Get;
            Head := Head mod Size + 1;
            Count := Count - 1;
         or
            terminate;
      end select;
   end loop;
end Bounded_Buffer;

Клиентская сторона: тоже с выбором

Симметрично серверу, клиент тоже может не ждать вечно. Условный вызов точки входа (select ... else) пытается встретиться, но если сервер не готов немедленно — выполняет ветку else. Таймированный вызов (select ... or delay) ждёт встречи не дольше заданного интервала. Это даёт устойчивость к зависаниям: клиент в системе реального времени не повиснет навсегда на недоступном сервере.

select
   Server.Deposit (100);          -- попытаться встретиться
or
   delay 0.5;                      -- но ждать не дольше полусекунды
   Ada.Text_IO.Put_Line ("Сервер не ответил вовремя");
end select;

Как работает под капотом

Рандеву реализуется через очереди ожидания у каждой точки входа. Когда клиент вызывает entry, а сервер ещё не дошёл до accept, клиент ставится в очередь этой точки входа и приостанавливается. Когда сервер достигает accept, он либо берёт первого из очереди (по умолчанию — в порядке поступления, FIFO), либо ждёт, если очередь пуста. Во время тела accept обе задачи синхронны: клиент заблокирован, сервер исполняет согласованный код. По завершении accept обе освобождаются. Сторожевые условия проверяются в момент достижения select: «закрытая» сторожем альтернатива не рассматривается, пока условие ложно. Этот механизм даёт высокоуровневую модель, в которой логика синхронизации выражена явно, а гонки данных по разделяемой памяти попросту не возникают — данные передаются через параметры встречи, а не через общие переменные.

Рандеву и наследие CSP

Модель рандеву не возникла на пустом месте — она опирается на теорию взаимодействующих последовательных процессов (CSP, Communicating Sequential Processes), сформулированную Тони Хоаром в 1978 году. Центральная идея CSP радикальна и красива: процессы не разделяют память, а общаются исключительно через синхронные сообщения. Нет общих переменных — нет и гонок данных по ним; вся координация выражена явными актами коммуникации. Рандеву Ada — прямое инженерное воплощение этой идеи: задачи синхронизируются в точке встречи и обмениваются данными через параметры точки входа, а не через разделяемое состояние. Тот же интеллектуальный фундамент позже породил каналы языков Occam и Go (chan, горутины) — так что, изучая рандеву, вы изучаете классическую модель, чьи потомки сегодня в мейнстриме.

Практическая ценность CSP-подхода в том, что он переводит рассуждения о параллелизме из плоскости «кто когда читает общую память» (где ошибки невидимы) в плоскость «кто с кем и когда обменивается сообщениями» (где взаимодействие явно и локализовано в коде). Когда вся координация выражена через entry/accept, поток управления параллельной программы можно проследить по тексту, а не вычислять в уме все возможные чередования доступов к переменным. Это делает конкурентный код Ada заметно более поддающимся анализу и проверке, чем код на разделяемой памяти с ручными блокировками.

Гибкость select и устойчивость к зависаниям

Оператор select заслуживает отдельного признания как инструмент построения отзывчивых, устойчивых систем. Его формы покрывают типичные потребности параллельных программ компактно и декларативно. Сторожевые условия (when) выражают «принимать этот запрос, только если состояние позволяет» — основа корректной синхронизации, как в ограниченном буфере. Альтернатива terminate даёт задаче-серверу штатный, безопасный способ завершиться, когда обслуживать больше некого, — без специальных флагов и сигналов. Таймированный вызов (select ... or delay) и условный вызов (select ... else) на стороне клиента защищают от вечного ожидания: в системе реального времени задача не повиснет навсегда на недоступном партнёре, а через заданный интервал перейдёт к запасному плану. Эти средства превращают рандеву из «просто синхронизации» в полноценный язык описания протоколов взаимодействия задач, устойчивых к сбоям и предсказуемых по времени.

Рандеву против защищённых объектов: критерий выбора

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

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

Подведём итог по механизмам взаимодействия. Рандеву и защищённые объекты — два дополняющих друг друга инструмента, и зрелость инженера видна в умении выбрать верный. Рандеву выражает диалог активных задач и сложные протоколы; защищённый объект безопасно и дёшево разделяет данные без отдельного потока. Оба устроены так, что опасные ошибки конкурентности в них структурно невозможны: нельзя забыть снять взаимное исключение, нельзя обойти барьер, нельзя обратиться к разделяемым данным в обход операций. Язык предоставляет не просто примитивы синхронизации, а дисциплину их безопасного применения, перенося рутину с программиста на компилятор и runtime. Именно поэтому конкурентный код на Ada надёжнее кода на сырых мьютексах и переменных условия: устранена не пара конкретных багов, а сама возможность целых их классов. Это прямое продолжение философии языка — безопасность по построению, а не по бдительности.

Частые ошибки

  • Забыть способ завершения сервера. Бесконечный loop с select без альтернативы terminate (или иного выхода) оставит задачу-сервер навсегда — и родитель не завершится. Альтернатива terminate; решает это корректно.
  • Класть много работы в тело accept. Пока идёт тело рандеву, клиент заблокирован. Делайте тело accept коротким (только обмен данными), а долгие вычисления выполняйте вне него.
  • Ожидать определённого порядка при нескольких готовых клиентах. select выбирает недетерминированно; не закладывайте конкретный порядок обслуживания разных альтернатив.
  • Висеть на вызове без тайм-аута в RT-системе. Для устойчивости используйте таймированный (select ... or delay) или условный (select ... else) вызов на клиентской стороне.

Итоги

  • Рандеву — синхронная встреча задач: сервер объявляет entry и принимает accept, клиент вызывает точку входа.
  • Данные передаются через параметры точки входа (in — клиент→сервер, out — обратно); во время тела accept клиент ждёт.
  • select позволяет серверу принимать разные обращения; альтернатива terminate корректно завершает задачу, сторожевые условия (when) открывают приём по условию.
  • Клиент может ограничить ожидание: условный (select ... else) и таймированный (select ... or delay) вызовы.
  • Под капотом — очереди ожидания у точек входа; данные идут через встречу, а не через разделяемую память, поэтому гонок по общим переменным не возникает.
Проверьте себя
1. Что происходит при рандеву, когда клиент вызвал точку входа, а сервер ещё не дошёл до соответствующего accept?
AВызов немедленно завершается с ошибкой
BКлиент ставится в очередь этой точки входа и ждёт, пока сервер дойдёт до accept
CСервер прерывает текущую работу и сразу обслуживает клиента
DДанные передаются через общую переменную без синхронизации
2. Зачем в операторе select задачи-сервера используют альтернативу terminate?
AЧтобы немедленно убить всех клиентов
BЧтобы сервер корректно завершился, когда обращаться к нему больше некому (все потенциальные клиенты ушли)
CЧтобы ускорить рандеву
DЧтобы запретить приём точек входа