Безопасная конкурентность в Ada — практическое руководство по задачам и защищённым объектам
· Го Комура · Ada, Конкурентность, Таскинг, Защищённые объекты, Рандеву, Реальное время, Параллельное программирование, Язык программирования, Параллелизм, Высокая надёжность
1. Введение — конкурентность, встроенная в язык
Конкурентность — неизбежная тема современной разработки программного обеспечения. Однако во многих языках конкурентность является «довеском»: она зависит от библиотек или возможностей ОС, и чтобы использовать её правильно, требуются глубокие знания и осторожное проектирование.
У Ada есть собственный ответ на эту проблему. Конкурентность встроена непосредственно в спецификацию языка.
Модель конкурентности Ada:
- Задача (task) — независимо выполняемая единица конкурентности
- Рандеву (rendezvous) — синхронный обмен между задачами
- Защищённый объект (protected object) — управляемое языком взаимное исключение
- Приоритеты реального времени — возможности Annex D для реального времени
Задачи и рандеву существуют начиная с Ada 83 (1983 год), защищённые объекты и возможности реального времени Annex D были добавлены в Ada 95, а затем эволюционировали в Ada 2005 и Ada 2012. Ada опирается не на низкоуровневые примитивы синхронизации вроде мьютексов и семафоров — её главная особенность в том, что «замысел проектирования можно выразить прямо в коде».
В этой статье мы последовательно разберём конкурентность в Ada на 8 практических примерах кода. Каждый пример — самостоятельный фрагмент, который можно скомпилировать и выполнить, и вы можете попробовать его у себя.
Фрагменты кода, встречающиеся в этой статье, опубликованы на GitHub в виде справочного набора, упорядоченного по главам.
ada-task-concurrency - komurasoft-blog-samples (GitHub)
2. Вспомним об «опасностях» конкурентности
Прежде чем перейти к Ada, кратко напомним, почему важна именно «безопасная» конкурентность.
К типичным ошибкам конкурентности относятся следующие.
- Гонка данных (data race): несколько потоков одновременно обращаются к одной и той же области памяти, причём хотя бы один — на запись. Результат не определён.
- Взаимная блокировка (deadlock): несколько задач бесконечно ждут завершения друг друга, и ни одна не может продвинуться дальше.
- Инверсия приоритетов (priority inversion): задача с высоким приоритетом ждёт ресурс, который удерживает задача с низким приоритетом, а задача со средним приоритетом при этом вытесняет задачу с низким приоритетом.
- Голодание (starvation): задача никогда не может получить нужный ей ресурс.
Модель конкурентности Ada предоставляет защиту на уровне языка от этих проблем.
Гонка данных → защищённый объект гарантирует эксклюзивный доступ
Взаимная блокировка → модель рандеву обеспечивает структурированную синхронизацию
Инверсия приоритетов → протокол Priority Ceiling Protocol доступен как встроенная возможность языка
Голодание → барьеры точек входа и политика очередей обеспечивают контроль
3. Основы задач — независимые единицы выполнения
Базовая единица конкурентности в Ada — это задача (task). Задача напоминает поток, но необязательно соответствует потоку ОС один к одному — планированием занимается runtime Ada.
task Greeter is
entry Start;
end Greeter;
task body Greeter is
begin
accept Start;
Put_Line ("Hello from a task!");
end Greeter;
Этот код (01_hello_task.ada) демонстрирует несколько важных моментов.
Задача начинает выполняться автоматически сразу после объявления. Задача Greeter запускается в момент, когда содержащая её процедура достигает begin, и ждёт на accept Start; запроса рандеву от вызывающей стороны.
Точка входа (entry) — это интерфейс, который задача предоставляет внешнему миру. Когда вызывающая сторона обращается к Greeter.Start;, происходит синхронизация с accept Start; задачи. Это называется рандеву.
Завершение задачи ожидается автоматически. Когда главная процедура завершается, если остаются ещё выполняющиеся задачи, их завершение неявно ожидается. Это разительно отличается от сбоев из-за забытого вызова std::thread::join в C++.
4. Рандеву — синхронный обмен с передачей данных
Рандеву — это не просто синхронизация, оно также позволяет передавать данные в обе стороны.
task Worker is
entry Compute (X, Y : Integer; Result : out Integer);
end Worker;
task body Worker is
A, B : Integer;
Output : Integer;
begin
accept Compute (X, Y : Integer; Result : out Integer) do
A := X;
B := Y;
Output := A * A + B * B;
Result := Output;
end Compute;
end Worker;
Вызывающая сторона использует его так (02_rendezvous_intro.ada).
Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));
Здесь важный момент проектирования — явно заданные режимы параметров.
- режим
in: передаёт значение от вызывающей стороны к задаче - режим
out: возвращает результат от задачи к вызывающей стороне - режим
in out: передаёт данные в обе стороны
Блок do ... end внутри accept становится критической секцией. Пока он выполняется, вызывающая сторона заблокирована, а задача не принимает другие точки входа. По завершении обработки обе стороны возобновляют работу.
Кратко об особенностях рандеву:
| Особенность | Описание |
|---|---|
| Синхронность | вызывающая сторона и задача ждут, пока обе не достигнут точки рандеву одновременно |
| Передача данных | параметры in / out / in out передают значения в обе стороны |
| Взаимное исключение | пока выполняется тело accept, другие точки входа этой задачи заблокированы |
| Структурированность | то, какие точки входа принимаются и когда, явно прописано в теле задачи |
5. Выборочный accept — ожидание нескольких сервисов
В реальных серверных задачах часто нужно ожидать запросы нескольких видов. Оператор select в Ada реализует это на уровне языка.
task Server is
entry Deposit (Amount : Integer);
entry Withdraw (Amount : Integer; Success : out Boolean);
entry Balance (Value : out Integer);
end Server;
task body Server is
Current : Integer := 0;
begin
loop
select
accept Deposit (Amount : Integer) do
Current := Current + Amount;
end Deposit;
or
accept Withdraw (Amount : Integer; Success : out Boolean) do
if Current >= Amount then
Current := Current - Amount;
Success := True;
else
Success := False;
end if;
end Withdraw;
or
accept Balance (Value : out Integer) do
Value := Current;
end Balance;
or
terminate;
end select;
end loop;
end Server;
В операторе select этого кода (03_selective_accept.ada) несколько ветвей or, и выбирается одна из точек входа, по которой уже есть вызов (сам выбор определяется реализацией). Если ни одна точка входа ещё не вызвана, задача ждёт, пока какую-либо из них не вызовут.
or terminate; — особая ветвь: она позволяет задаче безопасно завершиться, когда «главная процедура завершилась, и больше никто не может вызвать точку входа этой задачи». Это уникальный для Ada механизм, решающий проблему «сервер, ожидающий вечно», которая иначе приводит к взаимной блокировке.
Сильная сторона выборочного accept — возможность указывать защитные условия (guard conditions).
select
when Count > 0 =>
accept Get_Item (Item : out Integer) do
Item := Data (Head);
Count := Count - 1;
end Get_Item;
or
when Count < Max =>
accept Put_Item (Item : Integer) do
Data (Tail) := Item;
Count := Count + 1;
end Put_Item;
end select;
Ветвь, чьё защитное условие ложно, в этот момент исключается из выбора. Это позволяет декларативно описать, например, правило: «если буфер пуст, Get ждёт; если полон — ждёт Put».
6. Producer-consumer — синхронизация через рандеву
Рассмотрим типичный паттерн, использующий рандеву, — producer-consumer.
task Consumer is
entry Deliver (Item : Integer);
end Consumer;
task Producer;
task body Consumer is
Sum : Integer := 0;
begin
for I in 1 .. 5 loop
accept Deliver (Item : Integer) do
Sum := Sum + Item;
end Deliver;
end loop;
end Consumer;
task body Producer is
begin
for I in 1 .. 5 loop
Consumer.Deliver (I);
end loop;
end Producer;
В этом паттерне (04_producer_consumer.ada) каждый вызов Deliver со стороны производителя синхронизируется с потребителем. Если производитель работает слишком быстро, он ждёт, пока потребитель не выполнит accept; если слишком быстро работает потребитель, он ждёт следующего вызова производителя. Так естественным образом возникает обратное давление (backpressure).
7. Защищённые объекты — взаимное исключение без блокировок
Если задача — это «активный субъект действия», то защищённый объект (protected object) — это механизм для «пассивных разделяемых данных».
protected Counter is
procedure Increment;
function Value return Integer;
private
Count : Integer := 0;
end Counter;
protected body Counter is
procedure Increment is
begin
Count := Count + 1;
end Increment;
function Value return Integer is
begin
return Count;
end Value;
end Counter;
Ключевые правила защищённых объектов таковы.
- Функция (function) — только для чтения. Несколько задач могут вызывать функцию одновременно.
- Процедура (procedure) — для чтения и записи. Пока выполняется процедура, блокируются и другие процедуры, и функции.
- Точка входа (entry) — с барьером. Вызывающая сторона стоит в очереди, пока барьерное условие не станет истинным.
В этом коде (05_protected_counter.ada) три задачи-воркера вызывают Increment по 1000 раз каждая. Защищённый объект гарантирует взаимное исключение, поэтому итоговое значение счётчика всегда равно 3000. Вручную писать блокировку/разблокировку мьютекса не нужно.
task type Worker (Id : Integer; Rounds : Integer);
task body Worker is
begin
for I in 1 .. Rounds loop
Counter.Increment; -- защищённый объект гарантирует исключение
end loop;
end Worker;
W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);
Что произойдёт без защищённого объекта
Чтобы понять ценность защищённых объектов, рассмотрим опасный код, в котором данные не защищены.
-- ⚠ Опасно: прямая работа с разделяемой переменной
Shared_Counter : Integer := 0;
task body Bad_Worker is
begin
for I in 1 .. 10_000 loop
Shared_Counter := Shared_Counter + 1; -- гонка данных!
end loop;
end Bad_Worker;
Shared_Counter := Shared_Counter + 1 на уровне процессора — это три шага: «чтение → сложение → запись обратно». Когда несколько задач выполняют это одновременно, результат сложения одной задачи может не успеть попасть в память до чтения другой, и приращение теряется. Более того, это подпадает под понятие ошибочного выполнения (erroneous execution) согласно Ada RM 9.10: одновременное чтение и запись несинхронизированной разделяемой переменной может привести не просто к неточному итоговому значению счётчика, а к произвольному поведению всей программы. Даже если две задачи выполнят по 10 000 итераций каждая, итоговое значение вовсе не гарантированно окажется равным 20 000.
Защищённый объект предотвращает эту проблему «на уровне синтаксиса». Достаточно вызвать Counter.Increment; — компилятор и runtime сами гарантируют взаимное исключение.
8. Защищённые точки входа и барьеры — кольцевой буфер с ограничением
Добавление точек входа к защищённому объекту делает возможной условную синхронизацию. Рассмотрим классический кольцевой буфер с ограничением (bounded buffer).
type Buffer_Array is array (0 .. Buffer_Size - 1) of Integer;
protected Buf is
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Data : Buffer_Array;
Head : Integer := 0;
Tail : Integer := 0;
Count : Integer := 0;
end Buf;
protected body Buf is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Data (Tail) := Item;
Tail := (Tail + 1) mod Buffer_Size;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Data (Head);
Head := (Head + 1) mod Buffer_Size;
Count := Count - 1;
end Get;
end Buf;
when Count < Buffer_Size — это барьер (barrier). Барьер вычисляется при каждом вызове точки входа: если он истинен, выполнение продолжается, если ложен — вызывающая задача становится в очередь и ждёт. Каждый раз, когда состояние буфера меняется (другая задача выполняет Put или Get), барьеры ожидающих задач вычисляются заново.
Этот паттерн (06_bounded_buffer.ada) — один из случаев, где защищённые объекты Ada проявляют себя лучше всего. Сравните его с тем, как это пишется на C с использованием mutex + condition variable из pthread.
// Вариант на C + pthread (для сравнения с Ada)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) { // эквивалент when в Ada
pthread_cond_wait(¬_full, &mutex); // ожидание барьера
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty); // уведомление ожидающих задач
pthread_mutex_unlock(&mutex);
В Ada всё это сжимается в одну строку when Count < Buffer_Size. Условие цикла while, отправка сигнала, момент разблокировки — все возможности допустить ошибку в этих местах просто исчезают.
9. Вызовы с тайм-аутом — не ждать вечно
В системах реального времени «ждать вечно» недопустимо. Ada поддерживает тайм-ауты конструкцией select ... or delay.
select
Slow_Worker.Do_Work (Result);
Put_Line ("Main: work completed");
or
delay until Ada.Real_Time.Clock + Milliseconds (500);
Put_Line ("Main: timeout after 500ms!");
end select;
В этом коде (07_timed_entry.ada) Slow_Worker выполняет delay 2.0 и ещё не дошёл до accept, поэтому вызов точки входа, стоящий в очереди, завершается по тайм-ауту через 500 мс. (Тайм-аут действует на время ожидания в очереди до принятия рандеву, а не прерывает выполнение самого тела рандеву.) delay until задаёт абсолютное время — это базовая техника программирования реального времени, предотвращающая накопление дрейфа.
Кроме того, Ada поддерживает и условный вызов точки входа (conditional entry call).
select
Server.Process (Item);
else
Put_Line ("Server is busy, will retry later");
end select;
Ветка else позволяет немедленно перейти к альтернативной обработке, если рандеву невозможно выполнить сразу. Писать опрос (polling) вручную не требуется.
Не забывайте о проектировании после тайм-аута
Тайм-ауты удобны, но суть проектирования — в том, «что делать после того, как не дождались». Действительно ли можно отбросить значение, стоит ли повторить попытку, нужно ли сообщить об ошибке наверх — если оставить это неопределённым, в продакшене это превращается в потерю данных или остановку сервиса. Когда пишете тайм-аут, продумывайте ответственность за то, что происходит после него, в том же месте.
Периодические задачи и delay until
delay until полезен не только для тайм-аутов, но и для периодического выполнения. При простом delay 0.1 период получается равным «время обработки + 0.1 с», тогда как delay until задаёт следующую точку запуска абсолютным временем, поэтому период остаётся стабильным независимо от времени обработки.
loop
Next := Next + Period;
Do_Work;
delay until Next;
end loop;
Этот паттерн эффективен везде, где требуется строго периодическая обработка, — при опросе датчиков, в контурах управления и подобных задачах.
10. Приоритеты задач и планирование реального времени
Возможности реального времени в Ada определены в Annex D (Real-Time Systems). Если реализация Ada поддерживает Annex D, можно задавать приоритеты задач и политики планирования.
task High_Task is
pragma Priority (System.Default_Priority + 5);
end High_Task;
task Low_Task is
pragma Priority (System.Default_Priority);
end Low_Task;
Более продвинутая настройка позволяет задать также политику планирования и протокол верхнего предела приоритета.
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);
Priority Ceiling Protocol (протокол верхнего предела приоритета) — это протокол, предотвращающий инверсию приоритетов. Для каждого защищённого объекта явно задаётся предельный приоритет (ceiling priority) через pragma Priority (или аспект Priority). Если активный приоритет вызывающей задачи превышает этот предел, возникает Program_Error. Пока объект заблокирован, он выполняется на предельном приоритете, что предотвращает вытеснение задачами со средним приоритетом.
protected Shared_Data is
pragma Priority (15); -- предельный приоритет
procedure Update (Val : Integer);
function Read return Integer;
private
Data : Integer := 0;
end Shared_Data;
Эти возможности опираются на теоретическую базу Rate Monotonic Scheduling (RMS) и имеют подтверждённый опыт применения в жёстких системах реального времени — например, в системах управления полётом самолётов и в медицинском оборудовании.
11. Практические рекомендации по проектированию
До сих пор мы рассматривали базовый синтаксис задач и защищённых объектов. В заключение соберём рекомендации по проектированию, которые стоит учитывать при использовании конкурентности Ada на практике.
Чего нельзя делать внутри защищённого объекта
Внутри защищённого объекта действует железное правило: выполнять только короткое обновление состояния, а тяжёлую обработку переносить наружу. Операции защищённого объекта внутренне взаимно исключают друг друга, поэтому долгая блокировка внутри останавливает все остальные задачи, использующие тот же защищённый объект.
Конкретно следует избегать:
delayи медленного ввода-вывода;- сложных вызовов другого защищённого объекта;
- тяжёлых вызовов внешних библиотек.
При этом delay и определённые операции ввода-вывода внутри защищённого действия — это не просто вопрос производительности, а ограниченная ошибка (bounded error) по стандарту Ada. В зависимости от реализации это может привести к Program_Error или к взаимной блокировке, поэтому такие операции нужно не «стараться избегать», а полностью исключать.
Хороший паттерн проектирования: «быстро извлечь из защищённого объекта нужные значения → выполнить тяжёлые вычисления или ввод-вывод снаружи → быстро записать обратно в защищённый объект только результат».
Держите барьерные условия простыми
Барьер entry ... when <условие> — мощный инструмент, но если он становится слишком сложным, его трудно читать, и сложно понять, почему задача не освобождается.
Идеал — уровень, на котором смысл состояния понятен с первого взгляда, как в when Count < Buffer_Size или when Used > 0. Если действительно нужно несколько условий, стоит представить состояние через перечислимый тип и приблизить барьер к виду, читаемому по имени состояния, например when State = Running.
Исключения и остановка задач
Политику на случай возникновения исключения внутри задачи нужно определить явно. Как минимум, нужно перехватывать исключение на самом верхнем уровне тела задачи и фиксировать, что произошло.
Ещё важнее — проектирование того, что происходит после исключения. Сможет ли система продолжать работу, если эта задача остановится? Можно ли её перезапустить? Как уведомить другие задачи? Как вернуть разделяемое состояние в безопасное состояние? На эти вопросы нужно уметь ответить. У Ada есть языковой механизм исключений, но безопасность после исключения — ответственность проектирования приложения.
Мини-чек-лист проектирования
| Аспект | Что проверить |
|---|---|
| Разделяемое состояние | Заключено ли оно в защищённый объект? Нет ли прямого доступа извне? |
| Защищённые операции | Короткие ли они? Не блокируются ли внутри? |
| Точки входа | Прост ли барьер? Возможно ли бесконечное ожидание? Есть ли политика тайм-аута? |
| Время жизни задачи | Чётко ли определено условие завершения? Есть ли политика на случай исключения? |
| Периодическая обработка | Рассмотрен ли delay until вместо delay? |
В конкурентном программировании «наверное, всё в порядке» — самая опасная мысль. Явно фиксировать в коде разделяемое состояние, условия ожидания, условия завершения и политику исключений — это первый шаг к безопасной конкурентности.
12. Итог — язык, сделавший конкурентность «грамматикой»
Модель конкурентности Ada отличается от других языков тем, что безопасная конкурентность встроена не как «довесок из лучших практик», а как сама «грамматика» языка.
| Что нужно сделать | Синтаксис Ada |
|---|---|
| Независимая единица выполнения | task / task body |
| Синхронный обмен | entry / accept |
| Ожидание нескольких запросов | select / or / else |
| Взаимное исключение | protected / function / procedure |
| Условная синхронизация | entry ... when <барьер> |
| Тайм-аут | or delay until <время> |
| Управление приоритетом | pragma Priority |
Эти конструкции проверяются компилятором. Например, попытка изменить приватные компоненты самого защищённого объекта внутри его функции приводит к ошибке компиляции. Когда защищённая операция завершается, барьеры ожидающих точек входа автоматически пересматриваются — вручную отправлять сигнал не нужно.
«Подобно тому как система типов гарантирует безопасность памяти,
синтаксис конкурентности Ada гарантирует безопасность синхронизации»
Восемь примеров кода, рассмотренных в этой статье, — практическое введение в задачи, рандеву, защищённые объекты и возможности реального времени. Попробуйте запустить их у себя, а затем обратитесь к следующим более продвинутым темам.
- Профиль Ravenscar: ограничивающий профиль для задач, предназначенный для высоконадёжных систем реального времени. Ограниченная модель задач делает возможным статический анализ взаимных блокировок.
- Параллельные блоки Ada 2022: параллелизм данных с помощью конструкции
parallel ... do. - Интеграция со SPARK: формальная верификация поведения конкурентных программ (поддерживается GNATprove в рамках профиля Ravenscar).
И всё же «использовать Ada» не значит «быть в безопасности»
Последнее важное замечание. Синтаксис конкурентности Ada мощный, но использование Ada само по себе не делает программу безопасной автоматически. Прямая работа с разделяемыми данными в обход защищённого объекта, долгая блокировка внутри защищённого объекта, сложные взаимные вызовы между несколькими защищёнными объектами — такие ошибки проектирования возможны и в Ada.
Возможности языка спроектированы так, что «для опасного стиля кода требуется явное усилие», но они не выполняют проектирование за вас. Настоящая ценность Ada в том, что она позволяет приблизить обсуждение безопасности к самому коду — вопросы вроде «защищено ли это состояние?», «когда завершается эта задача?», «при каком условии ждёт эта точка входа?» можно оставить прямо в коде в виде синтаксиса.
Философия Ada — выражать проектирование через типы — последовательна и в вопросах конкурентности. Безопасная конкурентность начинается не с аккуратного обращения с блокировками, а с того, чтобы не допускать существования опасного разделяемого состояния «в открытом виде».
На расхожее мнение «конкурентность — это сложно» Ada отвечает: «если правильно выбрать синтаксис, безопасность гарантирует компилятор». Эта философия проектирования перекликается с современными языками вроде Rust и Pony, но Ada удерживает её в спецификации языка уже более 40 лет.
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Программирование систем реального времени на Ada — приоритеты, периодичность и контроль времени выполнения на практике
Изучаем Annex D Ada (системы реального времени) на 8 практических примерах кода: приоритеты задач, протокол Ceiling_Locking, периодическо...
Дженерики в Ada ── контракты через типы и повторное использование без накладных расходов
Систематическое руководство по обобщённому программированию в Ada: обобщённые подпрограммы и пакеты, формальные параметры-подпрограммы, к...
Введение в формальную верификацию с SPARK ── От контрактов Ada к математическому доказательству
Практическое введение в формальную верификацию с помощью SPARK — подмножества языка Ada. В статье рассматривается переход от контрактов (...
Притягательность языка Ada — типы как язык проектирования и опора для ПО, работающего десятилетиями
Рассказываем о притягательности языка Ada: строгая типизация, ограничения диапазона, разделение спецификации и реализации через пакеты, к...
Разделяемая память: подводные камни и практические рекомендации
Разбираем подводные камни практического использования разделяемой памяти и проектирование, снижающее частоту сбоев: синхронизация, видимо...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что такое задача (task) в Ada?
- Задача — это базовая единица конкурентности в Ada. Она напоминает поток (thread), но необязательно соответствует потоку ОС один к одному — планированием занимается runtime Ada. Задача начинает выполняться автоматически сразу после объявления, а когда завершается главная процедура, ещё выполняющиеся задачи неявно ожидаются до завершения. С внешним миром задача взаимодействует через синхронный обмен — рандеву — с помощью точек входа (entry). Задачи и рандеву входят в спецификацию языка ещё с Ada 83 (1983 год).
- Чем защищённые объекты в Ada отличаются от мьютексов?
- Защищённый объект — это механизм взаимного исключения, которым управляет сам язык: вручную писать lock/unlock не нужно. Функции (function) доступны только для чтения и могут вызываться несколькими задачами одновременно; процедуры (procedure) предназначены для чтения и записи — во время их выполнения блокируются все остальные вызовы; точки входа (entry) снабжены барьерным условием и ставят вызывающую сторону в очередь, пока условие не станет истинным. То, что на C пишется как комбинация pthread-мьютекса и условной переменной для управления кольцевым буфером с ограничением, в Ada сжимается до одной строки барьера вида «when Count < Buffer_Size».
- Как устроено рандеву в Ada?
- Рандеву — это механизм синхронного обмена между задачами: вызов точки входа на стороне вызывающего и оператор accept на стороне задачи ждут друг друга, пока оба не достигнут точки рандеву одновременно. Параметры с режимами in/out/in out позволяют передавать данные в обе стороны. Блок do...end внутри accept становится критической секцией: пока он выполняется, вызывающая сторона заблокирована, а задача не принимает другие точки входа. В сочетании с оператором select можно декларативно описать ожидание нескольких точек входа, тайм-ауты и защитные условия (guard conditions).
- Чего нельзя делать внутри защищённого объекта?
- Нельзя выполнять длительные блокирующие операции — delay, медленный ввод-вывод, тяжёлые вызовы внешних библиотек. delay и определённые операции ввода-вывода внутри защищённого действия относятся к ограниченной ошибке (bounded error) по стандарту Ada и, в зависимости от реализации, могут привести к возникновению Program_Error или к взаимной блокировке, поэтому их нужно полностью исключать, а не просто избегать по возможности. Золотое правило: внутри защищённого объекта делать только короткое обновление состояния, а тяжёлые вычисления и ввод-вывод выполнять снаружи, возвращая внутрь лишь результат — быстро и коротко.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки