← Назад к вопросам

В каком виде многозадачности потребляется больше ресурсов

2.0 Middle🔥 181 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Потребление ресурсов при разных видах многозадачности

Это часто неправильно понимается. Разные виды многозадачности потребляют ресурсы абсолютно по-разному. Разберу от наименьшего потребления к наибольшему.

Три вида многозадачности в 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:

  1. GIL (Global Interpreter Lock): только один поток выполняет Python код одновременно
  2. Context switching: процессор тратит время на переключение между потоками
  3. 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

Когда используется многопроцессность:

  1. CPU-bound задачи (когда нужны реальные параллельные вычисления)
  2. Обход 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 операций

Памятка: Если потребляешь больше памяти, чем нужно — вероятно, используешь неправильный инструмент.

В каком виде многозадачности потребляется больше ресурсов | PrepBro