Модуль 15

Task priorities та scheduling логіка

Ось твій урок у стилі CS50. Вмикай уяву, ніби ми зараз в аудиторії Sanders Theatre в Гарварді!


🎓 УРОК: Task Priorities & Scheduling Logic (Хто йде першим?)

1. 🔥 Вступ: проблема та мотивація

Уявіть, що ви працюєте лікарем у відділенні невідкладної допомоги (Emergency Room).

До вас приходить пацієнт, у якого болить горло. Ви починаєте його оглядати, виписувати рецепт, розмовляти про погоду... І в цей момент у двері завозять людину з серцевим нападом.

Питання до вас: Ви скажете: «Чекайте в черзі, я ще не закінчив із горлом»? Чи кинете все і побіжите рятувати життя?

Відповідь очевидна: ви переключитеся на критичного пацієнта.

А тепер уявіть ваш комп'ютер. Ви слухаєте музику, качаєте великий файл і одночасно рухаєте курсором миші. Якби процесор працював як «погана черга» (хто перший прийшов — того й обслуговуємо до кінця), то поки ваш файл не завантажиться, курсор миші навіть не поворухнувся б.

Чому без цього не обійтись? Процесор (CPU) у вас, швидше за все, має всього кілька ядер (наприклад, 4, 8 або 16). А процесів запущено сотні. Scheduling (Планування) — це мистецтво ілюзії. Це магія, яка змушує вас вірити, що все відбувається одночасно, хоча насправді процесор просто дуже швидко перемикається між задачами.

Без пріоритетів і правильного планування ваш смартфон перетворився б на цеглу, яка вміє робити тільки одну дію за раз.


2. 🧠 Теоретична база (без сухої академічності)

Давайте заглянемо «під капот» операційної системи. Тут є три головні дійові особи:

  1. Task (Завдання / Потік): Це наш «пацієнт». Шматок коду, який хоче, щоб його виконали.
  2. Resource (Ресурс/CPU): Це «лікар». Він один (або їх мало), і його час дуже цінний.
  3. 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)

Уявіть ситуацію:

  1. У черзі є завдання з пріоритетом 10 (низький).
  2. Але кожну мілісекунду приходять нові завдання з пріоритетом 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:

  1. Типова помилка новачка: «Я зроблю свій потік Real-Time Priority, щоб він літав!»
    • Реальність: Ви можете заблокувати мишку, клавіатуру і навіть системний таймер. Ваша програма літатиме, але ви не зможете її закрити, бо система не реагуватиме на Ctrl+C.
  2. Overhead (Накладні витрати): Перемикання між задачами — це дорого. Якщо ви розіб'єте роботу на мільйон мікро-задач, CPU витратить більше часу на перемикання між ними, ніж на саму роботу.
  3. Передбачуваність > Швидкість: У критичних системах (наприклад, гальма в машині) не так важливо, щоб процесор був супер-швидким, як те, щоб він гарантовано відреагував за 0.001 секунди.

Порада: Завжди залишайте важкі обчислення для низького пріоритету (background threads), а інтерфейс (UI) тримайте на високому. Користувач пробачить, якщо картинка вантажиться 2 секунди, але не пробачить, якщо кнопка "Скасувати" не натискається.


6. 🧩 Підсумок

Отже, що ми маємо в сухому залишку:

  • Планувальник — це бос, який вирішує, чий код крутиться на процесорі.
  • Пріоритети дозволяють системі бути чуйною (responsiveness).
  • Витискання (Preemption) дозволяє переривати неважливі задачі заради важливих.
  • Голодування лікується «старінням» задач.

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

Наступного разу: Ми заглибимося ще далі. Що станеться, якщо дві задачі одночасно спробують записати дані в одну й ту ж саму змінну? Готуйтеся, будемо говорити про Race Conditions та Deadlocks (смертельні обійми процесів).

Це був CS50. Побачимось! 👋