В чем разница между корутинами и потоками в Python?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между корутинами и потоками в Python
Корутины и потоки — это два разных подхода к конкурентному программированию в Python, имеющие кардинальные отличия в производительности, простоте использования и применении.
Потоки (Threading)
Потоки — это легковесные процессы, которые работают в контексте одного процесса, но выполняются параллельно на разных ядрах процессора.
import threading
import time
def worker(name):
for i in range(3):
print(f"{name}: итерация {i}")
time.sleep(1)
# Создание потоков
thread1 = threading.Thread(target=worker, args=("Thread-1",))
thread2 = threading.Thread(target=worker, args=("Thread-2",))
thread1.start()
thread2.start()
thread1.join() # Ожидание завершения
thread2.join()
Проблема GIL (Global Interpreter Lock): В CPython потоки не выполняются по-настоящему параллельно из-за GIL. Только один поток может выполнять Python код одновременно, поэтому потоки полезны только для I/O-bound задач.
Корутины (Async/Await)
Корутины — это функции, которые могут приостанавливаться и возобновляться, предоставляя асинхронное выполнение на одном потоке.
import asyncio
async def worker(name):
for i in range(3):
print(f"{name}: итерация {i}")
await asyncio.sleep(1) # Не блокирует поток
async def main():
# Одновременное выполнение корутин
await asyncio.gather(
worker("Coro-1"),
worker("Coro-2")
)
asyncio.run(main())
Ключевые отличия
| Аспект | Потоки | Корутины |
|---|---|---|
| Природа | Истинный параллелизм (ОС) | Кооперативная конкурентность |
| GIL | Ограничены GIL | Не затронуты GIL |
| Переключение | Вытесняющее (ОС решает) | Кооперативное (код решает) |
| Издержки | Высокие на создание/переключение | Низкие, легко создать 1000+ |
| I/O операции | Блокируют поток | Не блокируют, даёт ход другим |
| Синхронизация | Locks, Conditions (сложно) | asyncio API (просто) |
| Масштабируемость | До 100-200 потоков | До 100,000+ корутин |
Практический пример: I/O-bound задача
С потоками (неэффективно):
import threading
import requests
import time
def fetch_url(url):
response = requests.get(url)
print(f"Получен ответ: {response.status_code}")
return response
start = time.time()
threads = []
for url in urls:
t = threading.Thread(target=fetch_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"Потоки: {time.time() - start}s")
С корутинами (эффективно):
import asyncio
import aiohttp
import time
async def fetch_url(session, url):
async with session.get(url) as response:
print(f"Получен ответ: {response.status}")
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
start = time.time()
results = asyncio.run(main())
print(f"Корутины: {time.time() - start}s")
Когда использовать
Потоки:
- CPU-bound задачи (вычисления на многоядерных системах)
- Нужен истинный параллелизм
- Интеграция с блокирующими библиотеками
- Простые задачи с малым количеством потоков
Корутины:
- I/O-bound задачи (сетевые запросы, чтение файлов)
- Нужно обработать много одновременных операций
- Web-приложения и серверы (aiohttp, FastAPI)
- Масштабируемость и производительность
Гибридный подход
import asyncio
from concurrent.futures import ThreadPoolExecutor
# CPU-bound функция в отдельном потоке
def cpu_intensive(n):
return sum(i * i for i in range(n))
async def main():
loop = asyncio.get_event_loop()
executor = ThreadPoolExecutor()
# Выполнение CPU-bound в потоке
result = await loop.run_in_executor(executor, cpu_intensive, 1000000)
print(f"Результат: {result}")
asyncio.run(main())
Заключение
Потоки подходят для истинного параллелизма и CPU-bound задач, но ограничены GIL и издержками на переключение контекста. Корутины идеальны для I/O-bound задач с высокой конкурентностью и масштабируемостью. Выбор зависит от типа задачи и требований к производительности.