Ось твій урок у стилі CS50. Вмикай уяву, ніби ми зараз в аудиторії Sanders Theatre в Гарварді!
🎓 УРОК: Task Priorities & Scheduling Logic (Хто йде першим?)
1. 🔥 Вступ: проблема та мотивація
Уявіть, що ви працюєте лікарем у відділенні невідкладної допомоги (Emergency Room).
До вас приходить пацієнт, у якого болить горло. Ви починаєте його оглядати, виписувати рецепт, розмовляти про погоду... І в цей момент у двері завозять людину з серцевим нападом.
Питання до вас: Ви скажете: «Чекайте в черзі, я ще не закінчив із горлом»? Чи кинете все і побіжите рятувати життя?
Відповідь очевидна: ви переключитеся на критичного пацієнта.
А тепер уявіть ваш комп'ютер. Ви слухаєте музику, качаєте великий файл і одночасно рухаєте курсором миші. Якби процесор працював як «погана черга» (хто перший прийшов — того й обслуговуємо до кінця), то поки ваш файл не завантажиться, курсор миші навіть не поворухнувся б.
Чому без цього не обійтись? Процесор (CPU) у вас, швидше за все, має всього кілька ядер (наприклад, 4, 8 або 16). А процесів запущено сотні. Scheduling (Планування) — це мистецтво ілюзії. Це магія, яка змушує вас вірити, що все відбувається одночасно, хоча насправді процесор просто дуже швидко перемикається між задачами.
Без пріоритетів і правильного планування ваш смартфон перетворився б на цеглу, яка вміє робити тільки одну дію за раз.
2. 🧠 Теоретична база (без сухої академічності)
Давайте заглянемо «під капот» операційної системи. Тут є три головні дійові особи:
- Task (Завдання / Потік): Це наш «пацієнт». Шматок коду, який хоче, щоб його виконали.
- Resource (Ресурс/CPU): Це «лікар». Він один (або їх мало), і його час дуже цінний.
- Scheduler (Планувальник): Це «адміністратор лікарні». Саме він вирішує, кого пустити до лікаря наступним.
🔑 Що треба запам'ятати (Hard logic):
- Priority (Пріоритет): Число, яке показує важливість завдання. (Наприклад, 0 — найвищий, 10 — низький, або навпаки, залежить від системи).
- Context Switch (Перемикання контексту): Процес, коли CPU «зберігає стан» поточної задачі (запам'ятовує, на чому зупинився) і завантажує іншу. Це коштує ресурсів! Це не безкоштовно.
- Preemption (Витискання): Здатність зупинити задачу на півслові, щоб дати дорогу важливішій (як у прикладі з серцевим нападом).
🤔 Інтуїтивне розуміння:
Уявіть Планувальника як жонглера. Кульки — це завдання. Поки кулька в руці — вона виконується. Поки летить у повітрі — чекає. Якщо жонглер візьме занадто важку кульку (складну задачу) і триматиме її довго — всі інші впадуть.
Як це працює?
Планувальник має черги (queues). * High Priority Queue: Сюди потрапляють критичні задачі (рух миші, звук, драйвери). * Low Priority Queue: Сюди йде все інше (оновлення фонових програм, архівація).
Планувальник завжди перевіряє: «Чи є хтось у High Priority? Якщо так — виконую його. Якщо ні — берусь за Low».
3. 🧪 Приклади (від простого до реального)
Ми використаємо псевдокод (схожий на Python), щоб зрозуміти логіку, а не синтаксис.
Приклад 1: FIFO (First In, First Out) — Без пріоритетів
Уявіть чергу в магазині.
tasks = [
{"name": "Завантажити 4K відео", "duration": 100, "priority": "LOW"},
{"name": "Клік мишкою", "duration": 1, "priority": "HIGH"}
]
def run_scheduler_fifo(tasks):
for task in tasks:
print(f"Виконую: {task['name']}...")
time.sleep(task['duration']) # Імітація роботи
print("Готово!")
Питання до вас: Що станеться з "Кліком мишкою"? Відповідь: Користувач сказиться. Клік станеться тільки після того, як завантажиться все відео. Інтерфейс «зависне».
Приклад 2: Priority-Based Scheduling (З витисканням)
Тепер Планувальник розумніший.
# У нас є черга з пріоритетами
priority_queue = [
(10, "Рендеринг відео"), # Пріоритет 10 (низький)
(1, "Звук сповіщення") # Пріоритет 1 (високий)
]
# Сортуємо: чим менше число, тим вище пріоритет
priority_queue.sort(key=lambda x: x[0])
def run_smart_scheduler():
while priority_queue:
priority, task_name = priority_queue.pop(0) # Беремо перший елемент
print(f"🔥 CPU перехоплено: {task_name} (Пріоритет {priority})")
# Виконання...
Що ви очікуєте побачити? Спочатку виконається «Звук сповіщення», хоча в списку він був другим. Чому: Сортування (логіка планувальника) поставило важливе наперед.
Приклад 3: Проблема «Голодування» (Starvation)
Уявіть ситуацію:
- У черзі є завдання з пріоритетом 10 (низький).
- Але кожну мілісекунду приходять нові завдання з пріоритетом 5.
Що станеться із завданням 10? Воно ніколи не виконається. Це називається Starvation (Голодування). Бідна задача "помирає з голоду", чекаючи на процесорний час.
Рішення: Механізм Aging (Старіння). Логіка: Якщо задача довго чекає, ми штучно підвищуємо її пріоритет (наприклад, 10 -> 9 -> 8...). Рано чи пізно вона стане найважливішою і виконається.
4. 🛠 Практична частина
Зараз ви попрацюєте як архітектор системи.
🔹 Завдання 1: Ручний трейс Є задачі: A (пріоритет 2, час 10мс), B (пріоритет 1, час 5мс), C (пріоритет 3, час 20мс). У якому порядку вони виконаються і скільки часу пройде до моменту завершення задачі A?
🔹 Завдання 2: Зміна умов Уявіть, що задача B (високий пріоритет) «зависла» (нескінченний цикл). Що станеться з задачами A і C в системі з повним витисканням (preemptive)? Як це виправити на рівні Scheduler?
🔹 Завдання 3: Реальна задача (UI vs Network) Ви пишете месенджер. * Потік 1: Малює літери на екрані, коли користувач друкує. * Потік 2: Відправляє повідомлення на сервер. Розставте пріоритети (High/Low) і поясніть чому. Що буде, якщо переплутати?
🔹 Завдання 4: Міні-кейс (Ліфт) Ліфт у хмарочосі — це теж планувальник. Люди натискають кнопки на різних поверхах (це запити/задачі). * Варіант А: Ліфт їде до того, хто перший натиснув (FIFO). * Варіант Б: Ліфт підбирає всіх по дорозі вгору, потім усіх по дорозі вниз (SCAN алгоритм). Який алгоритм ефективніший і чому?
🔹 Питання «А що, якщо…» Що, якщо ми всім задачам поставимо найвищий пріоритет? (Спойлер: система стане такою ж повільною, як і з низьким, але ще й з накладними витратами на перемикання).
5. 💡 Мислення як у розробника
Як досвідчені інженери дивляться на Scheduling:
- Типова помилка новачка: «Я зроблю свій потік Real-Time Priority, щоб він літав!»
- Реальність: Ви можете заблокувати мишку, клавіатуру і навіть системний таймер. Ваша програма літатиме, але ви не зможете її закрити, бо система не реагуватиме на
Ctrl+C.
- Реальність: Ви можете заблокувати мишку, клавіатуру і навіть системний таймер. Ваша програма літатиме, але ви не зможете її закрити, бо система не реагуватиме на
- Overhead (Накладні витрати): Перемикання між задачами — це дорого. Якщо ви розіб'єте роботу на мільйон мікро-задач, CPU витратить більше часу на перемикання між ними, ніж на саму роботу.
- Передбачуваність > Швидкість: У критичних системах (наприклад, гальма в машині) не так важливо, щоб процесор був супер-швидким, як те, щоб він гарантовано відреагував за 0.001 секунди.
Порада: Завжди залишайте важкі обчислення для низького пріоритету (background threads), а інтерфейс (UI) тримайте на високому. Користувач пробачить, якщо картинка вантажиться 2 секунди, але не пробачить, якщо кнопка "Скасувати" не натискається.
6. 🧩 Підсумок
Отже, що ми маємо в сухому залишку:
- Планувальник — це бос, який вирішує, чий код крутиться на процесорі.
- Пріоритети дозволяють системі бути чуйною (responsiveness).
- Витискання (Preemption) дозволяє переривати неважливі задачі заради важливих.
- Голодування лікується «старінням» задач.
Тепер ви вмієте не просто писати код, а розумієте, як операційна система жонглює вашим кодом, щоб користувач був щасливий.
Наступного разу: Ми заглибимося ще далі. Що станеться, якщо дві задачі одночасно спробують записати дані в одну й ту ж саму змінну? Готуйтеся, будемо говорити про Race Conditions та Deadlocks (смертельні обійми процесів).
Це був CS50. Побачимось! 👋