Завдання (паралельне програмування)

Ну, ось ми дісталися і до завдань. Хотів укластися в одній частині, але не вийшло. Отже, завдання. Постараюся викласти простіше.

У нашому повсякденному житті багато подій відбуваються одночасно (паралельно). Вони майже завжди синхронізовані за часом, але певному етапі відбуваються одночасно. Наприклад, поїзди одночасно їдуть однією гілкою, літаки злітають і сідають одночасно, запити від багатьох користувачів до сайту теж відбуваються одночасно, касири в магазині можуть одночасно пробивати один і той же товар => одночасне звернення до бази даних, люди живуть поруч один з одним (паралельно) тощо. Для управління паралельними процесами зручно написати такі програми, які б дозволяли розподіляти навантаження між різними апаратними засобами та коректно зводити результат у єдину систему. Наприклад, якщо два, три або десять літаків одночасно заходять на посадку, то програма повинна одночасно обробляти інформацію про ці літаки і передавати їм коректні дані для посадки. Можна, звичайно, розподілити процесорний час між цими десятьма літаками та обробляти інформацію по кожному з них порційно на одному процесорі, але. якщо літаків буде не десять, а сто чи тисяча, то це вже буде проблема.

task My_First_Task; --анонімне завдання task type My_Second_Task; - Тип завдання task type My_Third_Task (n: Integer); --завдання прийматиме параметри (дискримінант)

task My_First_Task; task body My_First_Task is begin loop - нескінченний цикл Put_Line("Hello, World!"); end loop; end My_First_Task; . task type My_Third_Task(n: Integer); task body My_Third_Task is cnt : Integer := n; begin for i in 1..cnt loop Put(Item => i, Width => 3);end loop; end My_Third_Task;

Якщо після запуску цієї програми подивитися монітор ресурсів, то можна побачити, що вона виконується в три потоки: один потік - процедура example і два потоки - завдання:

паралельне

Усі три потоки виконуються незалежно один від одного. Ну а результат виконання виглядає приблизно так:

завдання

Тобто, незважаючи на те, що основна процедура очікує натискання кнопки, завдання виконуються незалежно від неї та один від одного.

Таким чином, можна запустити безліч завдань. Однак якщо у Вас один процесор і Ви створюєте два або більше завдань, це буде просто емулювання багатопотоковості. Чудес не буває 🙂

У прикладі ми використали оператор delay. Є ще один спосіб його використання:delay until :

Тип Time описаний у пакетах Ada.Calendar та Ada.Real_Time (при розробці систем реального часу краще використовувати останній пакет).

Для взаємодії завдань один з одним в Аді створено механізм Рандеву (фр. rendez-vous - зустріч, побачення). Розглянемо аналогію: нехай у нас є дві людини – хлопець та дівчина. Вони домовилися зустрітися (рандеву), і хлопець з квітами (інформація для обміну) прийшов на побачення трохи раніше за дівчину (перше завдання намагається обмінятися інформацією з другим завданням, але друге завдання не готове прийняти інформацію від першого). Хлопцеві (завданню один) нічого не залишається як очікувати приходу дівчини (очікувати готовності другого завдання до обміну інформацією. Ще раз повторюся, що завдання незалежні один від одного і їх не можна викликати як підпрограму у будь-який час). І тільки тоді, коли дівчина прийде (тобто друге завдання буде на тому етапі виконання (виконуватимуться ті інструкції), коли вона може обробити запит першого завдання), хлопецьподарує їй квіти (відбудеться рандеву та обмін інформацією).

Для того, щоб механізм рандеву працював, у задачі (в даному випадку в другому завданні) створюється як мінімум одинвхід, на якому вона готова чекати звернення до неї від іншого завдання (повертаючись до нашої аналогії - для того, щоб взяти квіти, у дівчини повинна бути як мінімум одна рука - один вхід.

Вхід оголошується аналогічно до процедури (включаючи режими доступу in, out, in out), тільки замість словаprocedure використовується словоentry:

--Тип завдання task type My_First_Task is --Завдання матиме одну точку входу Input entry Input(str : Unbounded_String; num : Integer); end My_First_Task;

У тілі завдання описи інструкцій входу використовується оператор прийомуaccept. Він означає, що завдання буде призупинено і перебуватиме в режимі очікування надходження запиту. За допомогоюaccept вказуються умови настання рандеву. Розглянемо приклад:

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Ada.Text_IO; use Ada.Text_IO; --with MyTask; procedure main is --Тип завдання task type My_First_Task is --Завдання матиме лише одну точку входу Input entry Input(str : Unbounded_String; num : Integer); end My_First_Task; - тіло завдання task body My_First_Task is s: Unbounded_String; n: Integer; begin --accept означає, що в цій точці завдання буде припинено --до тих пір, поки не отримає запит до точки входу Input accept Input(str : Unbounded_String; num : Integer) do --Завдання отримує на вході дві змінні. Їх потрібно скопіювати в локальні - Ось тут і відбувається рандеву s: = str; n: = Num; end Input; for i in 1..n loop Висновок переданого на вхід завдання повідомлення та етапу виконання завдання Put_Line(To_String(s) &Integer'Image(i)); delay 0.5; end loop; end My_First_Task; -Створюємо дві задачі mft_1: My_First_Task; mft_2: My_First_Task; begin - Тут обидві задачі почнуть своє виконання, але будуть призупинені в точках access - Для запуску завдань потрібно надіслати їм запит Input (він у задачах єдиний): mft_2.Input(To_Unbounded_String("Завдання 2. Етап"), 5); mft_1.Input(To_Unbounded_String("Завдання 1. Етап"), 10); end main;

Ну або якщо комусь простіше з вищеописаною аналогією про хлопця та дівчину:

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Ada.Strings.Unbounded.Text_IO; use Ada.Strings.Unbounded.Text_IO; procedure main is --завдання "Дівчина" task type girl is --вхід - взяти квіти entry GetFlowers(str : Unbounded_String); end girl; task body girl is flowers : Unbounded_String := To_Unbounded_String("немає кольорів"); begin --після виведення цього рядка завдання буде призупинено Put_Line("У мене" & flowers); --Рандеву - взяти квіти accept GetFlowers(str : Unbounded_String) do flowers := str; end GetFlowers; Put_Line("Тепер у мене є" & flowers); end girl; gl: girl; - Створити завдання - Завдання "Хлопець" task boy; task body boy is begin --Подарувати квіти (вхід завдання gl) gl.GetFlowers(To_Unbounded_String("ромашки")); delay 1.0; end boy; begin null; end main;

Завдання може мати кілька входів. Тоді для запобігання блокуванню в кожній інструкції accept в пеклі використовується інструкція відборуselect.

Нехай у нас є завдання з кількома входами:

task My_Task is entry Input_One(Параметри); --Параметрів може і не бути entry Input_Two(Параметри); entry Input_Three (Параметри); . entry Input_X(Параметри); end My_Task;

Тоді для можливості вибору (очікування) більше одного рандеву слід використовувати таку конструкцію:

task body My_Task is begin loop select accept Input_One(Параметри) do . end Input_One; or accept Input_Two(Параметри) do . end Input_Two; or accept Input_Three(Параметри) do . end Input_Three; or. or accept Input_X(Параметри) do . end Input_X; or terminate; --припинення завдання end select; end loop; end My_Task;

У цьому блоці циклічно відбувається перевірка входів щодо надходження запиту однією з них. Тут можна вказати умову завершення завдання, наприклад, при надходженні запиту на певний вхід або взагалі у разі ненадходження жодного запиту:

task body My_Task is begin loop select accept Input_One(Параметри) do . end Input_One; or. or accept Input_X(Параметри) do . end Input_X; or terminate; --припинення завдання end select; end loop; end My_Task;

Але найчастіше, як на мене, у процесі очікування рандеву завдання має виконувати якісь дії (якусь свою роботу). Для цього можна в конструкції select створити розділ else:

task body My_Task is begin loop select accept Input_One(Параметри) do . end Input_One; or. or accept Input_X(Параметри) do . end Input_X; else. --Якісь дії end select; end loop; end My_Task;

Також у конструкції select можна встановити інтервал часу, протягом якого будуть здійснюватися спроби встановити рандеву:

task body My_Task is begin loop select accept Input_One(Параметри) do . end Input_One; or. or accept Input_X(Параметри) do . end Input_X; or delay 3.0; -Спроби встановлення рандеву протягом 3 секунд. - Якісь дії, якщо рандеву не відбулося end select; end loop; end My_Task;

Всередині одного блокуselect може бути тільки одна з інструкцій:terminate,delay абоelse. А може й не бути жодної, ціінструкції є обов'язковими.

Можна організувати додаткову перевірку якихось умов перед прийняттям рандеву:

select when => accept Input_One(Параметри) do . end Input_One; or. end select;

Оператор selcet може використовуватися не тільки в задачі, що приймає, але і в зухвалій. Наприклад, якщо задача, що приймає, не готова встановити рандеву, то завдання, що викликає, буде в цей час здійснювати якісь дії:

select My_Task.Input_One(. ); else Put("Неможливо встановити рандеву"); end select;

Тобто використання select аналогічно як для приймаючої задачі, так і для зухвалої.