Задачи (task): параллелизм как часть языка
Задача (task) в Ada — это поток управления, встроенный прямо в язык: конкурентность здесь не библиотека поверх ОС, а первоклассная конструкция с собственным синтаксисом и семантикой.
Задача (task) — независимая единица параллельного исполнения внутри программы Ada: объявляется как самостоятельная сущность, имеет собственный поток управления и стек, запускается автоматически и выполняется конкурентно с остальной программой.
В большинстве языков параллелизм пришёл извне — как библиотека (pthreads в C, std::thread в C++) или как более поздняя надстройка. Ada поступила радикально иначе: конкурентность встроена в само определение языка с 1983 года. Это не случайность, а необходимость предметной области — встраиваемые системы и системы реального времени по своей природе параллельны: одновременно опрашиваются датчики, обновляются дисплеи, считаются управляющие воздействия. Сделав задачи частью языка, а не платформы, Ada получила переносимую, проверяемую компилятором и независимую от конкретной ОС модель параллелизма — то, чего годами не хватало системному программированию.
Зачем встраивать параллелизм в язык
Когда конкурентность — это библиотека, компилятор её «не понимает»: для него вызовы создания потоков и блокировок — обычные функции, чью корректность он проверить не может. Гонки данных, забытые разблокировки, неверный порядок захвата мьютексов остаются на совести программиста и всплывают как редкие, невоспроизводимые сбои. Встроив задачи в язык, Ada дала компилятору знание о параллелизме: он видит, где начинается и заканчивается задача, как задачи общаются, какие данные разделяются. Это открывает дорогу к статическим проверкам и к безопасным по построению механизмам взаимодействия (рандеву и защищённые объекты — темы следующих уроков), которые исключают целые классы ошибок конкурентности на уровне конструкций языка.
Второе преимущество — переносимость и определённость. Семантика задач задана стандартом, а не нравом конкретной библиотеки потоков. Программа с задачами ведёт себя предсказуемо на разных платформах, а для жёсткого реального времени стандарт описывает специальные приложения (Real-Time Systems Annex) с гарантиями планирования, недостижимыми через сырые потоки ОС.
Объявление и запуск задачи
Как и пакет, задача имеет две части: спецификацию (объявляет задачу и её точки взаимодействия) и тело (код, который задача исполняет). Простейшая задача без точек взаимодействия объявляется и запускается автоматически — её тело начинает выполняться, как только «оживёт» окружающая область.
with Ada.Text_IO; use Ada.Text_IO;
procedure Two_Tasks is
task Worker_A; -- спецификация (без точек входа)
task body Worker_A is
begin
for I in 1 .. 3 loop
Put_Line ("A:" & I'Image);
delay 0.01; -- уступить время, имитируя работу
end loop;
end Worker_A;
task Worker_B;
task body Worker_B is
begin
for I in 1 .. 3 loop
Put_Line ("B:" & I'Image);
delay 0.01;
end loop;
end Worker_B;
begin
-- Обе задачи УЖЕ запущены параллельно с этим телом.
Put_Line ("Главная процедура работает тоже");
end Two_Tasks;
Ключевая особенность: задачи Worker_A и Worker_B стартуют автоматически при оживлении процедуры Two_Tasks и выполняются конкурентно и друг с другом, и с основным телом. Точный порядок строк вывода не определён — он зависит от планировщика; это нормальное свойство параллельной программы. Оператор delay 0.01; приостанавливает задачу на указанное время (в секундах), давая другим задачам шанс выполниться.
Жизненный цикл задачи и зависимость
Задача проходит стадии: создание (при оживлении), активация (старт тела), выполнение, завершение. Важнейшее правило Ada — иерархия зависимости: задача, объявленная внутри блока, процедуры или другой задачи, считается зависимой от этой области, и область не может завершиться, пока не завершатся все зависимые от неё задачи. Это структурный, предсказуемый параллелизм: нельзя «случайно» оставить висящий поток, переживший родителя.
procedure Parent is
task Child;
task body Child is
begin
delay 0.05;
Ada.Text_IO.Put_Line ("Child закончил");
end Child;
begin
Ada.Text_IO.Put_Line ("Parent дошёл до конца тела");
-- Parent НЕ завершится здесь: он ЖДЁТ завершения Child.
end Parent; -- выход произойдёт только после "Child закончил"
Это правило резко отличает Ada от языков, где поток-демон легко переживает создавшую его функцию. Здесь время жизни задачи структурно вложено во время жизни её области — как локальная переменная. Такой «структурированный параллелизм» предотвращает утечки потоков и упорядочивает завершение программы.
Как работает под капотом
За задачами стоит среда исполнения времени выполнения (runtime), включающая планировщик. В полнофункциональной конфигурации задачи Ada обычно отображаются на потоки ОС, и планирование делегируется операционной системе с учётом приоритетов, заданных средствами Real-Time Annex. Но Ada не привязана к «тяжёлым» потокам ОС: для встраиваемых систем существуют компактные runtime, реализующие задачи поверх собственного планировщика на «голом железе» без операционной системы вовсе. Спецификация задачи отделена от тела ровно для того, чтобы взаимодействие (точки входа, о которых речь в следующем уроке) можно было объявить как контракт, а реализацию скрыть. Оператор delay — это обращение к планировщику «не давай мне процессор до истечения интервала», и его относительная (delay D;) и абсолютная (delay until T;) формы — основа периодического исполнения в системах реального времени.
Почему встроенный параллелизм безопаснее библиотечного
Различие между «параллелизм как часть языка» и «параллелизм как библиотека» стоит разобрать глубже, потому что оно объясняет всю модель Ada. Когда конкурентность приходит библиотекой (pthreads в C, потоки поверх ОС), компилятор воспринимает создание потока и захват блокировки как обычные вызовы функций — для него это непрозрачные операции, чью семантику он не знает. Поэтому он не может предупредить о гонке данных, о захвате блокировок в неверном порядке (источник взаимоблокировок), о забытом освобождении мьютекса. Вся ответственность ложится на программиста, а ошибки конкурентности коварны: они проявляются редко, зависят от тайминга и почти невоспроизводимы при отладке.
Встроив задачи в язык, Ada дала компилятору понимание параллелизма. Он видит границы задач, знает, какие данные разделяются и через какие конструкции происходит взаимодействие. Это знание — фундамент, на котором строятся высокоуровневые безопасные механизмы: рандеву и защищённые объекты (темы следующих уроков) устроены так, что целые классы ошибок в них попросту невыразимы. Нельзя «забыть освободить» защищённый объект — взаимное исключение снимается автоматически при выходе из операции. Нельзя обратиться к разделяемым данным «в обход» — доступ только через защищённые операции. Язык не просто предоставляет потоки; он предоставляет дисциплину их безопасного использования.
Переносимость и независимость от платформы
Есть и второе важное следствие. Семантика задач определена стандартом языка, а не нравом конкретной библиотеки или особенностями ОС. Программа с задачами ведёт себя предсказуемо при переносе между платформами, потому что её модель параллелизма — часть языка, а не тонкого слоя над системными вызовами. Более того, Ada не привязана к «тяжёлым» потокам операционной системы: одна и та же программа с задачами может исполняться поверх потоков ОС на рабочей станции и поверх компактного собственного планировщика на «голом железе» микроконтроллера без всякой ОС. Стандартное приложение реального времени (Real-Time Systems Annex) добавляет переносимые средства управления приоритетами и планированием, недостижимые через сырые потоки. Так встроенный параллелизм даёт то, чего годами не хватало системному программированию: единую, переносимую, проверяемую компилятором модель конкурентности.
Задачи как активные объекты предметной области
Полезно мыслить задачи не как «потоки ОС», а как активные сущности предметной области — это меняет стиль проектирования к лучшему. В системе управления реальные параллельные процессы естественно отображаются на задачи: отдельная задача опрашивает датчик температуры, другая управляет насосом, третья ведёт журнал, четвёртая обслуживает интерфейс оператора. Каждая задача инкапсулирует один независимый поток деятельности со своей логикой и темпом. Программа становится прямым отражением структуры реального мира, где эти процессы и впрямь идут одновременно, а не искусственно сводятся к одному последовательному циклу с ручным переключением.
Такой взгляд проясняет и проектные решения. Если две деятельности по своей природе независимы и идут параллельно — это две задачи. Если же нечто лишь охраняет общие данные для других, не имея собственной активности, — это не задача, а защищённый объект (пассивный страж). Различение «активное поведение против пассивной охраны данных» — ключ к грамотной архитектуре параллельной программы на Ada. Задачи моделируют то, что делает что-то само по себе; защищённые объекты — то, что лишь безопасно хранит и выдаёт состояние. Когда это различие проведено чётко, конкурентная программа получается ясной, а её поведение — анализируемым, потому что структура кода совпадает со структурой параллелизма в задаче.
Закрепим, чем встроенный параллелизм Ada отличается от привычного библиотечного. Делая задачи частью языка, Ada дала компилятору понимание конкурентности — а значит, и возможность строить безопасные по построению механизмы взаимодействия (рандеву, защищённые объекты), где целые классы ошибок невыразимы. Структурированная зависимость задач исключает висящие потоки; семантика, заданная стандартом, обеспечивает переносимость между потоками ОС и «голым железом»; приложение реального времени добавляет управляемое планирование. Программа на Ada моделирует параллельные процессы предметной области напрямую, а её структура совпадает со структурой реального параллелизма — что делает конкурентный код яснее и анализируемее, чем нагромождение ручных блокировок над разделяемой памятью. Это фундамент, на котором стоят следующие, более высокоуровневые механизмы раздела, и причина, по которой Ada десятилетиями остаётся языком выбора для встраиваемых систем и систем реального времени.
Частые ошибки
- Думать, что задачу надо «запускать» вручную. Задача стартует автоматически при оживлении её области; отдельного «start» не требуется.
- Полагаться на порядок вывода параллельных задач. Порядок чередования определяется планировщиком и не гарантирован; не закладывайте в логику конкретную последовательность строк.
- Ожидать, что родитель завершится, бросив задачи. Область ждёт завершения всех зависимых задач; это структурная гарантия, а не блокировка по ошибке.
- Использовать активное ожидание (busy-wait) вместо
delay. Пустой цикл «ждать» сжигает процессор;delayи механизмы синхронизации уступают время корректно.
Итоги
- Задача (
task) — встроенная в язык единица параллельного исполнения со своим потоком и стеком. - Параллелизм в Ada — часть языка, а не библиотека: компилятор «понимает» его, что открывает дорогу безопасным механизмам взаимодействия.
- Задача имеет спецификацию и тело; задачи без точек входа запускаются автоматически при оживлении области.
- Действует структурированный параллелизм: область не завершается, пока живы зависимые от неё задачи, — нет висящих потоков.
- Runtime отображает задачи на потоки ОС или на собственный планировщик «голого железа»;
delayкорректно уступает процессор.