Skip to content

32. (Л) Основи асинхронного програмування та конкурентності в Python

Зміст лекції

  1. Конкурентність vs паралелізм
  2. Моделі конкурентності в Python
  3. Процеси та потоки: базові концепції
  4. GIL — Global Interpreter Lock
  5. Коли що використовувати
  6. Огляд модуля 3

Конкурентність vs паралелізм

Ці два поняття часто плутають, але між ними є принципова різниця.

Конкурентність (Concurrency) — це здатність програми мати кілька задач у процесі виконання одночасно. Задачі можуть чергуватися, але в один момент часу виконується лише одна з них.

Паралелізм (Parallelism) — це справжнє одночасне виконання кількох задач на різних процесорних ядрах.

graph TD
    subgraph "Конкурентність (1 ядро)"
        T1A["Задача A ██░░░░██░░░░"]
        T1B["Задача B ░░██░░░░██░░"]
    end
    subgraph "Паралелізм (2 ядра)"
        T2A["Задача A ████████████"]
        T2B["Задача B ████████████"]
    end

Аналогія

Уявіть кухаря. Конкурентність — один кухар готує кілька страв по черзі: поки одна вариться, він ріже іншу. Паралелізм — два кухарі готують страви одночасно, кожен свою.

Типи задач за характером навантаження

Тип Назва Приклади Вузьке місце
I/O-bound Навантаження введення/виведення HTTP-запити, читання файлів, БД Очікування відповіді
CPU-bound Навантаження на процесор Математичні обчислення, стиснення, шифрування Час процесора
graph LR
    subgraph "I/O-bound задача"
        A1["Надіслати запит"] --> W1["⏳ Очікування..."] --> B1["Отримати відповідь"]
    end
    subgraph "CPU-bound задача"
        A2["Почати обчислення"] --> C2["⚙️ ⚙️ ⚙️ Активна робота"] --> B2["Результат"]
    end

Вибір інструменту конкурентності залежить від типу задачі:

  • I/O-bound → потоки (threading) або asyncio
  • CPU-bound → процеси (multiprocessing)

Моделі конкурентності в Python

Python надає три основні моделі:

graph TD
    PY["Python Concurrency"] --> TH["threading<br/>Потоки"]
    PY --> MP["multiprocessing<br/>Процеси"]
    PY --> AI["asyncio<br/>Асинхронність"]

    TH --> TH1["Один процес<br/>Кілька потоків<br/>Спільна пам'ять"]
    MP --> MP1["Кілька процесів<br/>Окрема пам'ять<br/>Справжній паралелізм"]
    AI --> AI1["Один потік<br/>Подієвий цикл<br/>Кооперативна багатозадачність"]

    style TH fill:#339af0,stroke:#333,color:#fff
    style MP fill:#51cf66,stroke:#333,color:#000
    style AI fill:#ffd43b,stroke:#333,color:#000
Модель Паралелізм Пам'ять Накладні витрати Найкраще для
threading Обмежений (GIL) Спільна Низькі I/O-bound задачі
multiprocessing Справжній Ізольована Високі CPU-bound задачі
asyncio Конкурентність Спільна Мінімальні Багато I/O-bound задач

Процеси та потоки: базові концепції

Процес

Процес — це незалежна програма, що виконується операційною системою. Кожен процес має:

  • власний адресний простір пам'яті
  • власні ресурси (файлові дескриптори тощо)
  • щонайменше один потік виконання
graph TD
    OS["Операційна система"]
    OS --> P1["Процес 1<br/>Python interpreter<br/>Пам'ять: 50 MB"]
    OS --> P2["Процес 2<br/>Python interpreter<br/>Пам'ять: 50 MB"]
    OS --> P3["Процес 3<br/>Python interpreter<br/>Пам'ять: 50 MB"]

    style OS fill:#868e96,stroke:#333,color:#fff
    style P1 fill:#339af0,stroke:#333,color:#fff
    style P2 fill:#339af0,stroke:#333,color:#fff
    style P3 fill:#339af0,stroke:#333,color:#fff

Переваги процесів:

  • справжній паралелізм (кожен на своєму ядрі)
  • ізоляція — збій одного процесу не впливає на інші

Недоліки процесів:

  • велике споживання пам'яті
  • складний обмін даними між процесами (IPC)
  • повільне створення

Потік

Потік (thread) — це одиниця виконання всередині процесу. Кілька потоків одного процесу:

  • спільно використовують пам'ять та ресурси
  • виконуються «одночасно» (з перемиканням)
graph TD
    P["Процес Python<br/>Спільна пам'ять"]
    P --> T1["Потік 1<br/>(main thread)"]
    P --> T2["Потік 2"]
    P --> T3["Потік 3"]
    T1 & T2 & T3 --> MEM["Heap: об'єкти Python<br/>Глобальні змінні"]

    style P fill:#339af0,stroke:#333,color:#fff
    style MEM fill:#ffd43b,stroke:#333,color:#000

Переваги потоків:

  • швидке створення (дешевше за процеси)
  • легкий обмін даними через спільну пам'ять

Недоліки потоків:

  • GIL обмежує паралелізм у CPython
  • гонки даних (race conditions) при спільному доступі

Порівняння на прикладі

import time


def task(name: str, duration: float) -> None:
    print(f"{name}: початок")
    time.sleep(duration)  # Імітація I/O
    print(f"{name}: завершення")


# Послідовне виконання — найповільніше
start = time.time()
task("A", 2)
task("B", 2)
task("C", 2)
print(f"Послідовно: {time.time() - start:.1f}с")  # ~6.0с

З потоками або asyncio ті самі три задачі виконаються приблизно за 2 секунди — ми детально розглянемо це в наступних лекціях.

GIL — Global Interpreter Lock

М'ютекс (mutex, від mutual exclusion — взаємне виключення) — це примітив синхронізації, який захищає спільний ресурс від одночасного доступу кількох потоків. М'ютекс працює як замок: перший потік, що його «захоплює», отримує доступ до ресурсу, а всі інші потоки, що намагаються захопити той самий м'ютекс, змушені чекати, поки він не буде звільнений.

GIL (Global Interpreter Lock) — це м'ютекс у CPython, який дозволяє виконуватись лише одному потоку Python в один момент часу.

sequenceDiagram
    participant T1 as Потік 1
    participant GIL as GIL
    participant T2 as Потік 2

    T1->>GIL: Захопити GIL
    GIL-->>T1: ✅ Дозволено
    Note over T1: Виконує Python-код
    T1->>GIL: Звільнити GIL (I/O або ~100 інструкцій)
    T2->>GIL: Захопити GIL
    GIL-->>T2: ✅ Дозволено
    Note over T2: Виконує Python-код
    T2->>GIL: Звільнити GIL

Чому існує GIL?

CPython управляє пам'яттю через підрахунок посилань (reference counting). Без GIL два потоки могли б одночасно змінювати лічильник, що призвело б до помилок пам'яті.

# Внутрішньо кожен об'єкт Python має лічильник
import sys

x = [1, 2, 3]
print(sys.getrefcount(x))  # Кількість посилань на об'єкт

Вплив GIL на різні типи задач

graph LR
    subgraph "CPU-bound: потоки НЕ допомагають"
        C1["Потік 1: обчислення"] -->|"захоплює GIL"| CG["GIL"]
        C2["Потік 2: чекає GIL"] -.->|"блокований"| CG
    end
    subgraph "I/O-bound: потоки ДОПОМАГАЮТЬ"
        I1["Потік 1: I/O"] -->|"звільняє GIL"| IG["GIL"]
        I2["Потік 2: виконується"] -->|"захоплює GIL"| IG
    end

GIL і CPU-bound задачі

Для CPU-bound задач потоки в CPython не дають прискорення і навіть можуть сповільнити програму через накладні витрати на перемикання. Для справжнього паралелізму обчислень використовуйте multiprocessing.

GIL і I/O-bound задачі

Під час очікування I/O (мережа, диск) потік звільняє GIL. Тому threading ефективний для I/O-bound задач: поки один потік чекає відповіді від сервера, інший може виконуватись.

Альтернативи без GIL

  • PyPy — альтернативна реалізація Python з JIT (власний підхід до GIL)
  • Python 3.13+ — експериментальний режим без GIL (--disable-gil)
  • Cython, C extensions — можуть звільняти GIL для обчислювального коду

Коли що використовувати

graph TD
    Q1{"Яка задача?"}
    Q1 -->|"I/O-bound"| Q2{"Скільки з'єднань?"}
    Q1 -->|"CPU-bound"| MP["multiprocessing"]

    Q2 -->|"Десятки"| TH["threading"]
    Q2 -->|"Сотні / тисячі"| AI["asyncio"]

    style MP fill:#51cf66,stroke:#333,color:#000
    style TH fill:#339af0,stroke:#333,color:#fff
    style AI fill:#ffd43b,stroke:#333,color:#000

Практичні приклади вибору

Задача Рекомендація Причина
Завантаження 10 файлів із мережі threading або asyncio I/O-bound, просто
Обробка зображень (resize 1000 фото) multiprocessing CPU-bound
Веб-сервер з тисячами з'єднань asyncio Масштабованість
Парсинг 50 сайтів asyncio Багато I/O-bound
Шифрування великих файлів multiprocessing CPU-bound
Простий скрипт з 2–3 запитами Послідовний код Не варто ускладнювати

Золоте правило

Не додавайте конкурентність без потреби. Послідовний код простіший у розробці та налагодженні. Додавайте конкурентність лише тоді, коли є вимірювана потреба в продуктивності.

Огляд модуля 3

До кінця модуля ви зможете:

  • запускати задачі паралельно за допомогою threading та multiprocessing
  • писати асинхронний код із asyncio
  • завантажувати дані з мережі конкурентно
  • вимірювати та порівнювати продуктивність різних підходів
  • налагоджувати асинхронні програми

Підсумок

Концепція Опис
Конкурентність Кілька задач у процесі виконання, чергуються
Паралелізм Кілька задач виконуються одночасно на різних ядрах
I/O-bound Задача, що чекає на введення/виведення
CPU-bound Задача, що активно використовує процесор
GIL М'ютекс CPython — один потік Python в один момент
threading Потоки в одному процесі, ефективні для I/O-bound
multiprocessing Окремі процеси, справжній паралелізм для CPU-bound
asyncio Один потік, подієвий цикл, тисячі конкурентних I/O-bound задач

Ключові принципи:

  • Розрізняйте I/O-bound і CPU-bound — від цього залежить вибір інструменту
  • GIL обмежує потоки для CPU-bound — для обчислень використовуйте процеси
  • asyncio масштабується краще — але потребує асинхронних бібліотек
  • Послідовний код завжди простіший — додавайте конкурентність лише за потреби

Корисні посилання


Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub