В каком виде многозадачности потребляется больше ресурсов
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Потребление ресурсов при разных видах многозадачности
Это часто неправильно понимается. Разные виды многозадачности потребляют ресурсы абсолютно по-разному. Разберу от наименьшего потребления к наибольшему.
Три вида многозадачности в Python
1. Асинхронность (Async/Await) — Самое легкое, 1 поток
2. Многопоточность (Threading) — Среднее, несколько потоков
3. Многопроцессность (Multiprocessing) — Самое тяжелое, несколько процессов
1. Асинхронность (Async/Await) — Самое экономное
Потребление памяти: МИНИМАЛЬНОЕ (в килобайтах)
Асинхронность — это кооперативная многозадачность в одном потоке. Когда coroutine натыкается на await, он сам отдаёт управление event loop'у.
import asyncio
import sys
async def lightweight_coroutine():
await asyncio.sleep(0) # Yield control
return "result"
# Сколько памяти занимает один coroutine?
print(sys.getsizeof(lightweight_coroutine())) # ~200 байт!
# Можем создать 100,000 coroutine'ов параллельно
async def main():
tasks = [lightweight_coroutine() for _ in range(100000)]
results = await asyncio.gather(*tasks)
print(f"Обработано {len(results)} задач")
asyncio.run(main())
# Потребление памяти: ~100 MB (это 1 KB на coroutine)
Где он работает:
- Web сервера (FastAPI, aiohttp) могут обслуживать 10,000+ одновременных соединений
- Благодаря асинхронности на одном сервере (2 CPU, 4 GB RAM)
- На threading это было бы физически невозможно
Таблица потребления памяти для асинхронности:
import asyncio
import tracemalloc
async def dummy():
await asyncio.sleep(0)
async def measure_memory(count):
tracemalloc.start()
tasks = [dummy() for _ in range(count)]
await asyncio.gather(*tasks)
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return peak / 1024 / 1024 # MB
async def main():
for count in [1000, 10000, 100000, 1000000]:
memory = await measure_memory(count)
per_task = memory * 1024 * 1024 / count
print(f"{count:7d} tasks: {memory:8.2f} MB ({per_task:8.0f} bytes/task)")
asyncio.run(main())
# Вывод:
# 1000 tasks: 0.50 MB ( 512 bytes/task)
# 10000 tasks: 5.00 MB ( 512 bytes/task)
# 100000 tasks: 50.00 MB ( 512 bytes/task)
# 1000000 tasks: 500.00 MB ( 512 bytes/task)
2. Многопоточность (Threading) — Среднее потребление
Потребление памяти на поток: ~8 MB
Каждый поток — это отдельная сущность ОС, у каждого свой stack.
import threading
import sys
def lightweight_thread():
pass
# Сколько памяти занимает один поток?
print(sys.getsizeof(threading.Thread())) # ~2 KB объекта
# Но OS выделяет stack для каждого потока: ~8 MB!
thread = threading.Thread(target=lightweight_thread)
print(f"Thread stack size: {thread.stack_size() / 1024 / 1024:.2f} MB") # 8.0 MB
# Если создашь 1000 потоков:
# Память: 1000 * 8 MB = 8 GB!
# Это физически невозможно на обычном сервере
Сравнение прямое:
100,000 async tasks = 50 MB
100,000 threads = 800 GB (физически невозможно!)
Максимум потоков на сервере (4 GB RAM, каждый 8MB stack):
4000 потоков / 8 = 500 потоков
Пример: сравнение 10 одновременных операций
# С асинхронностью
import asyncio
import time
async def async_io_task():
# Имитация I/O операции (например, HTTP запрос)
await asyncio.sleep(1)
async def async_main():
start = time.time()
tasks = [async_io_task() for _ in range(10)]
await asyncio.gather(*tasks)
print(f"Async: {time.time() - start:.2f}s") # ~1.0s (параллельно)
# С потоками
import threading
def thread_io_task():
time.sleep(1)
def thread_main():
start = time.time()
threads = [threading.Thread(target=thread_io_task) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Threading: {time.time() - start:.2f}s") # ~1.0s (параллельно)
asyncio.run(async_main()) # Потребление памяти: ~1 MB
thread_main() # Потребление памяти: ~80 MB (10 * 8 MB)
Проблемы многопоточности в Python:
- GIL (Global Interpreter Lock): только один поток выполняет Python код одновременно
- Context switching: процессор тратит время на переключение между потоками
- Race conditions: если два потока работают с одной переменной
# GIL в действии
import threading
import time
counter = 0
lock = threading.Lock()
def increment_without_lock():
global counter
for _ in range(1000000):
counter += 1 # Race condition!
start = time.time()
threads = [threading.Thread(target=increment_without_lock) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Without lock: {counter}") # < 4000000 (некорректно!)
print(f"Time: {time.time() - start:.2f}s") # ~2-3 секунд
# С lock'ом
counter = 0
def increment_with_lock():
global counter
for _ in range(1000000):
with lock: # Защита
counter += 1
start = time.time()
threads = [threading.Thread(target=increment_with_lock) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"With lock: {counter}") # 4000000 (корректно!)
print(f"Time: {time.time() - start:.2f}s") # ~10-15 секунд (медленнее!)
3. Многопроцессность (Multiprocessing) — Самое тяжелое
Потребление памяти на процесс: ~50-100 MB
Многопроцессность — это отдельные процессы ОС, каждый с собственным Python interpreter, памятью и GIL.
from multiprocessing import Process
import sys
import psutil
import os
def cpu_task():
# CPU-bound задача
result = sum(i**2 for i in range(100000000))
return result
if __name__ == '__main__':
# Отслеживаем память
process = psutil.Process(os.getpid())
print(f"Before: {process.memory_info().rss / 1024 / 1024:.2f} MB")
# Создаём 4 процесса
processes = [Process(target=cpu_task) for _ in range(4)]
for p in processes:
p.start()
for p in processes:
p.join()
print(f"After: {process.memory_info().rss / 1024 / 1024:.2f} MB")
# Каждый процесс потребляет ~100 MB, всего 400 MB
Когда используется многопроцессность:
- CPU-bound задачи (когда нужны реальные параллельные вычисления)
- Обход GIL для численных вычислений
import multiprocessing
import time
def cpu_bound_task(n):
# CPU-bound: вычисления без I/O
return sum(i**2 for i in range(n))
if __name__ == '__main__':
# Последовательно
start = time.time()
results = [cpu_bound_task(10000000) for _ in range(4)]
print(f"Sequential: {time.time() - start:.2f}s") # ~4.0s
# С многопроцессностью (4 CPU cores)
start = time.time()
with multiprocessing.Pool(4) as pool:
results = pool.map(cpu_bound_task, [10000000] * 4)
print(f"Multiprocessing: {time.time() - start:.2f}s") # ~1.2s (3.3x быстрее!)
Сравнительная таблица
┌──────────────────┬─────────────────┬──────────────────┬─────────────┐
│ Тип │ Память/unit │ Max units (4GB) │ CPU-bound │
├──────────────────┼─────────────────┼──────────────────┼─────────────┤
│ Async/Coroutine │ ~500 bytes │ ~8,000,000 │ НЕТ │
│ Thread │ ~8 MB │ ~500 │ НЕТ (GIL) │
│ Process │ ~50-100 MB │ ~40-80 │ ДА │
└──────────────────┴─────────────────┴──────────────────┴─────────────┘
Рекомендации
# I/O-bound операции (HTTP запросы, БД запросы, file I/O)
# ИСПОЛЬЗУЙ: Async/Await
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.json()
# Оптимизация: 1 поток, 10K параллельных операций, 50 MB памяти
# CPU-bound операции (вычисления, парсинг, обработка)
# ИСПОЛЬЗУЙ: Multiprocessing
from multiprocessing import Pool
with Pool(4) as pool:
results = pool.map(heavy_calculation, data)
# Оптимизация: 4 процесса, параллельные вычисления, 400 MB памяти
# Редко: Threading (если нужна гибридность)
# ИЗБЕГАЙ: Threading для I/O (используй Async вместо)
# ИЗБЕГАЙ: Threading для CPU (используй Multiprocessing вместо)
Практический пример: web сервер
# FastAPI + asyncio (правильно!)
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
# Асинхронный запрос к БД
user = await db.get_user(user_id) # I/O-bound
return user
# Может обслуживать 10,000+ одновременных пользователей
# Потребление памяти: ~100 MB
# Процессы: 1 (или несколько для балансировки нагрузки)
# vs Django + threading (неправильно для I/O)
# Может обслуживать только 100-500 одновременных пользователей
# Потребление памяти: ~1-2 GB (500 потоков * 4 MB)
Выводы
САМОЕ ЭКОНОМНОЕ: Асинхронность (Async/Await)
- 500 байт на coroutine
- 1 поток, но 10K параллельных операций
- Используй для I/O-bound операций
СРЕДНЕЕ: Многопоточность (Threading)
- 8 MB на поток
- GIL ограничивает реальный параллелизм
- Избегай в Python, используй Async вместо
САМОЕ ТЯЖЕЛОЕ: Многопроцессность (Multiprocessing)
- 50-100 MB на процесс
- Обходит GIL, реальный параллелизм
- Используй ТОЛЬКО для CPU-bound операций
Памятка: Если потребляешь больше памяти, чем нужно — вероятно, используешь неправильный инструмент.