Ответ
Основное различие заключается в том, кто и когда принимает решение о переключении между задачами.
Кооперативная многозадачность (Cooperative Multitasking)
В этой модели задача сама добровольно передает управление другой задаче в явно указанном месте. В Python это реализуется с помощью asyncio
.
- Кто переключает: Сама программа (задача) через ключевое слово
await
. - Преимущества: Минимальные накладные расходы на переключение контекста, так как не требуется вмешательство ОС. Отсутствие гонки данных (race conditions) в рамках одного потока.
- Недостатки: Одна долго выполняющаяся (блокирующая) задача, которая не передает управление, может «подвесить» весь цикл событий (event loop).
Пример (asyncio
):
import asyncio
async def say_hello():
print("Начало hello")
await asyncio.sleep(1) # Явная передача управления
print("Привет!")
async def say_world():
print("Начало world")
await asyncio.sleep(1) # Явная передача управления
print("Мир!")
asyncio.run(asyncio.gather(say_hello(), say_world()))
Вытесняющая многозадачность (Preemptive Multitasking)
В этой модели операционная система (ОС) сама решает, когда прервать выполнение одной задачи и передать управление другой. В Python это реализуется с помощью потоков (threading
).
- Кто переключает: Операционная система.
- Преимущества: Блокировка одной задачи не останавливает остальные. Возможность параллельного выполнения на нескольких ядрах процессора (с оговоркой на GIL).
- Недостатки: Более высокие накладные расходы на переключение контекста. Требуется использование механизмов синхронизации (блокировки, семафоры) для предотвращения гонки данных.
Пример (threading
):
import threading
import time
def print_number(num):
time.sleep(1) # ОС может переключить поток в любой момент
print(f"Поток {num}")
threads = [threading.Thread(target=print_number, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
Важно: В CPython из-за Global Interpreter Lock (GIL) вытесняющая многозадачность на потоках не дает реального параллелизма для задач, интенсивно использующих CPU. Однако она эффективна для I/O-bound задач (ожидание сети, диска), так как GIL освобождается на время ожидания.