
- •Определения
- •Виды циклов Безусловные циклы
- •Цикл с предусловием
- •Цикл с постусловием
- •Цикл с выходом из середины
- •12. Опишіть переваги циклічного алгоритму.
- •13. Опишіть алгоритм розподілу часу.
- •14. Опишіть алгоритм кооперативна багатозадачність.
- •15. Опишіть алгоритм пріоритетна багатозадачність із витисненням.
14. Опишіть алгоритм кооперативна багатозадачність.
Кооперативна багатозадачність
Мабуть, найпростішою реалізацією багатозадачної системи була б бібліотека підпрограм, яка визначає наступні процедури.
struct Thread; У тексті обговорюватиметься, що має бути ця структура, звана дескриптором нитки.
Thread * ThreadCreate(void (*ThreadBody)(void)); Створити нитку, виконуючу функцію ThreadBody.
void Threadswitch(); Ця функція припиняє поточну нитку і активізує чергову, готову до виконання.
void ThreadExit () ; Припиняє виконання поточної нитки.
Зараз ми не обговорюємо методів синхронізації ниток і взаємодії між ними (для синхронізації були б корисні також функції void DeactivateThread(); І void ActivateThread(struct Thread *);). Нас цікавить лише питання: що ж ми повинні зробити, аби перемкнути нитки? функція ThreadSwitch називається диспетчером або планувальником (scheduler) і поводиться таким чином.
Вона передає управління на наступну активну нитку.
Поточна нитка залишається активна, і через деякий час знову отримає управління.
При цьому вона отримає управління так, як ніби ThreadSwitch була звичайною функцією і повернула управління в крапку, з якої вона була викликана.
Вочевидь, що функцію ThreadSwitch не можна реалізувати на мові високого рівня, начеб З, тому що це має бути функція, яка не повертає [негайно] управління в ту крапку, з якої вона була викликана. Вона викликається з однієї нитки, а передає управління в іншу. Це вимагає прямих маніпуляцій стеком і записом активізації і зазвичай досягається використанням асемблера або асемблерних вставок. Деякі ЯВУ (Ada, Java, Occam) надають примітиви створення і перемикання ниток у вигляді спеціальних синтаксичних конструкцій. Найпростішим варіантом, здавалося б, буде проста передача управління на нову нитку, наприклад, командою безумовної передачі управління по покажчику. При цьому весь описувач нитки (struct Thread) складатиметься лише з адреси, на яку треба передати управління. Беда лише в тому, що цей варіант не працюватиме. Дійсно, кожна з ниток виконує програму, що складається з вкладених викликів процедур. Для того, щоб нитка нормально продовжила виконання, нам потрібно відновити не лише адресу поточної команди, але і стек викликів (див. разд. Непрямо-регістровий режим із зсувом). Тому ми приходимо до такої архітектури.
Кожна нитка має свій власний стек викликів.
При створенні нитки виділяється область пам'яті під стек, і покажчик на цю область поміщається в дескриптор нитки.
ThreadSwitch зберігає покажчик стека (і, якщо такий є, покажчик кадру) поточної нитки в її дескрипторі і відновлює SP з дескриптора наступної активної нитки (перемикання стеків необхідно реалізувати асемблерною вставкою, тому що мови високого рівня не надають засобів для прямого доступу до покажчика стека (приклад 8.1)).
Коли функція ThreadSwitch виконує оператора return, вона автоматично повертає управління в те місце, з якого вона була викликана в цій нитці, тому що адреса повернення зберігається в стеку.
Приклад 8.1. Кооперативний перемикач потоків
Thread * thread_queue_head; Thread * thread_queue_tail; Thread * current_tread; Thread * old__thread; void TaskSwitch () { old_thread=current_thread; add_to_queue_tail(current_thread); current_thread=get_from_queue_head(); asm { . move bx, old_thread push bp move ах, sp move thread_sp[bx], ах move bx, current_thread move ах, rhread_sp[bx] pop bp } return; }
Якщо система програмування передбачає, що при виклику функції повинні зберігатися певні регістри (як, наприклад, з-компілятори для х86 зберігають при викликах регістри SI і DI (ESI/EDI в 1386)), то вони також зберігаються в стеку. Тому запропонований нами варіант також автоматично зберігатиме і відновлюватиме всі необхідні регістри. Зрозуміло, що окрім покажчиків стека і стекового кадру struct Thread повинна містити ще деякі поля. Як мінімум, вона повинна містити покажчик на наступну активну нитку. Система повинна зберігати покажчики описувач поточної нитки і на кінець списку. При цьому ThreadSwitch переставляє поточну нитку в кінець списку, а поточною робить наступну за нею в списку. Всі нитки, що знов активізуються, також ставляться в кінець списку. При цьому список не зобов'язаний бути двонаправленим, адже ми витягуємо елементи лише з початку, а додаємо лише в кінець. Часто в літературі такий список називають чергою ниток (thread queue) або чергою процесів. Така черга присутня у всіх відомих авторові реалізаціях багатозадачних систем. Крім того, черги ниток використовуються і при організації черг чекання різних подій, наприклад, при реалізації семафорів Дейкстри. Планувальник, заснований на Threadswitch тобто на принципі перемикання за ініціативою активної нитки, використовується у ряді експериментальних і учбових систем. Цей же принцип, званий кооперативною багатозадачністю реалізований в бібліотеках мов Simula 67 і Modula-2. MS Windows 3.x також мають засіб для організації кооперативного перемикання завдань — системний виклик GetNextEvent. Часто кооперативні нитки називають не нитками, а співпрограмами — адже вони викликають один одного, подібно до підпрограм. Єдина відмінність такого виклику від виклику процедури полягає в тому, що такий виклик не ие-рархичен — викликана програма може знов передати управління початковою і залишитися при цьому активною. Основною перевагою кооперативної багатозадачності є простота відладки планувальника. Крім того, знімаються всі колізії, пов'язані з критичними секціями і тому подібними труднощами, — адже нитка може просто не віддавати нікому управління, поки не буде готова до цього. З іншого боку, кооперативна багатозадачність має і серйозні недоліки. По-перше, необхідність включати в програму виклики Threadswitch ускладнює програмування взагалі і перенесення програм з однозадачних або інакше організованих багатозадачних систем зокрема. Особливо неприємна вимога регулярно викликати Threadswitch для обчислювальних програм. Найчастіше такі програми виконують відносно короткий внутрішній цикл, швидкість роботи якого визначає швидкість всієї програми. Для "плавної" багатозадачності необхідно викликати Threadswitch з тіла цього циклу. Робити виклик на кожному кроці Циклу недоцільно, тому необхідно буде написати код, схожий на приведений в прикладі 8.2. Приклад 8.2. Внутреній цикл програми в кооперативно багатозадачному середовищі
int counter; // змінна-лічильник while(condition){ // Викликати ThreadSwitch кожні rate циклів. counter++; if (counter % rate == 0) ThreadSwitch(); .... // Власне обчислення j }
Умовний оператор і виклик функції у внутрішньому циклі сильно ускладнюють роботу оптимізуючим компіляторам і наводять до розривів конвеєра команд, що може дуже помітно понизити продуктивність. Виклик функції на кожному кроці циклу наводить до ще більших накладних витрат і, відповідно, до ще більшого уповільнення. По-друге, зловмисна нитка може захопити управління і нікому не віддавати його. Просто не викликатиThreadSwitch і все. Це може статися не лише із-за злих намірів, але і просто помилково. Тому така схема виявляється непридатна для розрахованих на багато користувачів систем і часто не дуже зручна для інтерактивних розрахованих на одного користувача. Чомусь більшість комерційних програм для Win16, у тому числі і що поставлялися самою фірмою Microsoft, недостатньо активно використовували виклик GetNextEvent. Замість цього такі програми монопольно захоплювали процесор і малювали відомі всім користувачам цієї системи "пісочний годинник". В цей час система ніяк не реагує на запити і інші дії користувача окрім натиснення кнопки RESET або клавіш <CTRL>+<ALT>+<DEL>. По-третє, кооперативна ОС не може виконуватися на симетричній багатопроцесорній машині, а додатки, написані з розрахунку на таку ОС, не можуть скористатися перевагами многопроцессорности. Простий аналіз показує, що кооперативні багатозадачні системи придатні лише для учбових проектів або тих ситуацій, коли програмістові на швидку руку необхідно створити багатозадачне ядро. Друга ситуація здається декілька дивною — навіщо для серйозної роботи може потрібно швидко зроблене ядро, якщо існує багато готових систем реального часу, а також загальнодоступних (freeware або public domain) у вигляді вихідних текстів реалізацій таких ядер?