32. (Л) Основи асинхронного програмування та конкурентності в Python¶
Зміст лекції¶
- Конкурентність vs паралелізм
- Моделі конкурентності в Python
- Процеси та потоки: базові концепції
- GIL — Global Interpreter Lock
- Коли що використовувати
- Огляд модуля 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 масштабується краще — але потребує асинхронних бібліотек
- Послідовний код завжди простіший — додавайте конкурентність лише за потреби
Корисні посилання¶
- Python docs — threading
- Python docs — multiprocessing
- Python docs — asyncio
- Real Python — Python Concurrency
- PEP 703 — Making the Global Interpreter Lock Optional
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub