Модуль 9

Workers та concurrency models

Ось готовий урок, згенерований у стилі CS50, спеціально для тебе. Приготуй каву (або чай), бо зараз ми розберемося, як не змушувати твій код "чекати".


🎓 CS50 Style: Workers та Concurrency Models

Привіт, друзі! Мене звати [Твоє Ім'я], і це... урок про те, як робити кілька справ одночасно і не збожеволіти.

Сьогодні ми говоримо про Workers (Воркери) та Concurrency (Конкурентність).


1. 🔥 Вступ: Чому черга за кавою — це погано?

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

Ви підходите до каси, замовляєте еспресо. Бариста приймає оплату, повертається до кавомашини, меле зерна, варить каву, наливає, віддає вам. Це займає 2 хвилини. Наступний клієнт хоче складний лате з кокосовим молоком. Бариста знову: оплата -> приготування (5 хвилин) -> видача.

Питання до вас: Що відбувається з чергою, поки бариста збиває кокосове молоко? Правильно. Черга стоїть. Люди нервують. Хтось запізнюється на роботу.

У світі програмування це називається Synchronous Blocking Execution (Синхронне блокуюче виконання). Якщо ваш веб-сервер (бариста) почне обробляти важке завдання (лате), він перестане відповідати всім іншим користувачам. Сайт "зависне".

Чому без цієї теми не обійтись? Тому що в реальному світі користувачі не будуть чекати 10 секунд, поки ваш сервер згенерує PDF-звіт або обробить відео. Вони просто підуть до конкурентів. Нам потрібно навчитися приймати замовлення миттєво, а "варити каву" — у фоновому режимі.


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

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

1. Main Thread (Головний потік) — Офіціант

Це "обличчя" вашої програми. Офіціант бігає залом, приймає замовлення і приносить рахунки. Головне правило: Офіціант ніколи не готує їжу! Якщо офіціант піде на кухню смажити стейк на 20 хвилин, нові гості в залі будуть ігноровані.

2. The Worker (Воркер) — Кухар

Це спеціальний процес (або потік), який живе "на кухні" (у фоні). Він не спілкується з клієнтами напряму. Він просто бере замовлення зі списку і мовчки робить важку роботу.

3. The Queue (Черга повідомлень) — Дошка замовлень

Офіціант записує замовлення на папірець і вішає на цвях на кухні. Офіціант одразу біжить назад у зал. Кухар, коли звільниться, знімає папірець і починає готувати.

Конкурентність (Concurrency) vs Паралелізм (Parallelism)

Ось тут часто плутаються. Давайте просто:

  • Concurrency (Конкурентність): Це коли ви вмієте менеджерити багато завдань одночасно. Офіціант прийняв замовлення у столика №1, потім у №2, потім відніс страву №1. Він перемикається між задачами.
  • Parallelism (Паралелізм): Це коли задачі виконуються буквально в один і той самий момент. Це коли на кухні стоять два кухарі і смажать два стейки одночасно.

Що треба запам’ятати залізно: Важкі операції (обробка фото, надсилання тисячі email, складні розрахунки) НІКОЛИ не повинні відбуватися в головному потоці (Main Thread). Ми завжди скидаємо їх на Воркера.


3. 🧪 Приклади (Від болю до краси)

Давайте подивимося на код (наприклад, на Python, бо він читабельний, але логіка однакова для JS, Go, Java).

Приклад 1: Блокуючий код (Як робити не треба) ❌

Уявіть функцію, яка обробляє завантажене зображення.

import time

def process_image(image_name):
    print(f"Починаю обробку {image_name}...")
    time.sleep(5)  # Імітуємо важку роботу (5 секунд)
    print(f"Готово: {image_name} оброблено!")

# Користувач 1 завантажує фото
process_image("avatar.jpg")

# Користувач 2 хоче просто завантажити сторінку...
print("Вітаємо на сайті!") 

Питання: Коли користувач 2 побачить напис "Вітаємо на сайті"? Відповідь: Тільки через 5 секунд! Весь світ зупинився заради avatar.jpg. Це катастрофа для UX.


Приклад 2: Використання Воркера (The CS50 Way) ✅

Тепер уявімо, що ми маємо систему черг (наприклад, Redis Queue або просто окремий потік).

import threading
import time

# Функція, яку буде виконувати Воркер (Кухар)
def worker_task(image_name):
    print(f"🧑‍🍳 Воркер: Взяв у роботу {image_name}...")
    time.sleep(5)
    print(f"✅ Воркер: {image_name} готовий!")

# Головний потік (Офіціант)
print("🤵 Офіціант: Прийняв замовлення на avatar.jpg")

# Створюємо воркера і даємо йому задачу
bg_worker = threading.Thread(target=worker_task, args=("avatar.jpg",))
bg_worker.start() # "Запускаємо" кухаря, але не чекаємо його!

print("🤵 Офіціант: Вітаємо наступного клієнта! Чого бажаєте?")

Що ми побачимо в консолі?

  1. 🤵 Офіціант: Прийняв замовлення...
  2. 🤵 Офіціант: Вітаємо наступного клієнта... (МИТТЄВО!)
  3. 🧑‍🍳 Воркер: Взяв у роботу...
  4. ...(через 5 секунд)...
  5. ✅ Воркер: ...готовий!

Бачите різницю? Головний потік звільнився за мілісекунди. Користувач щасливий. Воркер працює собі тихо в куточку.


4. 🛠 Практична частина

Час закачати рукави. Спробуйте розв'язати ці задачі (усно або в коді):

  1. 🔹 Повтори: Напиши скрипт, де "Головний потік" друкує числа від 1 до 10 кожну секунду, а "Воркер" у цей час завантажує "великий файл" (просто чекає 3 секунди і пише "Завантажено"). Вони мають працювати одночасно.
  2. 🔹 Зміни умови: Що станеться, якщо у вас два воркера, але лише один ресурс (наприклад, один принтер)? Як зробити так, щоб вони не друкували на одному аркуші одночасно? (Підказка: черга).
  3. 🔹 Виправ помилку: У вас є змінна counter = 0. Два воркера одночасно роблять counter += 1 тисячу разів. Чому в кінці counter може бути не 2000, а, скажімо, 1985? (Це називається Race Condition).
  4. 🔹 Міні-кейс: Ви розробляєте Instagram. Користувач заливає відео.
    • Крок 1: Зберегти оригінал.
    • Крок 2: Створити версію 720p.
    • Крок 3: Створити версію 1080p.
    • Крок 4: Надіслати сповіщення "Відео готове".
    • Завдання: Розпишіть, що робить Main Thread, а що роблять Workers. Чи можна робити кроки 2 і 3 одночасно?

5. 💡 Мислення як у розробника

Як відрізнити новачка від сеньйора в цій темі?

⛔️ Новачок: * Запускає важкі розрахунки прямо в контролері веб-сайту. * Думає: "Та це швидко, всього 2 секунди, ніхто не помітить". (Спойлер: помітять). * Забуває обробляти помилки у воркера. Якщо воркер "впав" під час обробки платежу, гроші зникли?

🧠 Досвідчений розробник: * "Fire and Forget" (Вистрілив і забув): Якщо результат не потрібен миттєво — у воркер його. * Idempotency (Ідемпотентність): А що, якщо воркер випадково виконає задачу двічі? (Наприклад, двічі надішле списання коштів). Досвідчений пише код так, щоб друге виконання нічого не ламало. * Monitoring: Він завжди знає, чи живі його воркери. Бо якщо черга росте, а воркери "здохли", бізнес стоїть.

Порада: Завжди думайте про "найгірший сценарій". Що буде, якщо світло вимкнеться, коли воркер зробив роботу наполовину?


6. 🧩 Підсумок

Отже, що ми сьогодні зрозуміли:

  1. Main Thread — для швидкої взаємодії з користувачем (UI/Requests).
  2. Workers — для важкої, брудної, довгої роботи.
  3. Concurrency дозволяє програмі залишатися чуйною (responsive), навіть коли вона зайнята.

Тепер ви вмієте будувати архітектуру, яка не "гальмує". Ваші програми більше не схожі на кав'ярню з одним сонним баристою. Ви будуєте McDonald's, де каса окремо, а кухня окремо.

🔜 У наступній серії: Ми згадали проблему, коли два воркери лізуть до однієї змінної (Race Condition). Як захистити дані? Ми поговоримо про Locks, Mutex та Semaphores. Це як світлофори для ваших потоків.

А поки що — це був CS50 style. Кодіть із задоволенням!