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

Можно ли I/O bound задачу решить с помощью многопроцессинга?

1.0 Junior🔥 171 комментариев
#Soft Skills

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

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

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

Можно ли I/O bound задачу решить с помощью многопроцессинга?

Технически — можно, но это неоптимально. Для I/O bound задач многопроцессинг — неправильный инструмент.

Типы задач в Python

CPU-bound (CPU интенсивные)

Задачи, где узким местом является процессор:

  • Обработка больших чисел
  • Математические вычисления
  • Обработка изображений
  • ML модели

Решение: Многопроцессинг (multiprocessing) — обходит GIL

I/O bound (I/O интенсивные)

Задачи, где узким местом является ввод-вывод:

  • Сетевые запросы (HTTP, API)
  • Чтение/запись файлов
  • Запросы к БД
  • WebSocket соединения

Решение: Асинхронность (asyncio) или многопоточность (threading)

Почему многопроцессинг неоптимален для I/O bound

Проблема 1: Высокие накладные расходы

import multiprocessing
import time
import requests
from concurrent.futures import ProcessPoolExecutor

def fetch_url(url):
    """I/O bound функция — ждёт ответ от сервера"""
    response = requests.get(url)
    return len(response.text)

urls = ['https://example.com'] * 100

# ❌ Многопроцессинг для I/O (плохо)
start = time.time()
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(fetch_url, urls))
print(f"Многопроцессинг: {time.time() - start:.2f}s")
# Результат: 45 секунд (много overhead на создание процессов)

# ✅ Асинхронность для I/O (хорошо)
import asyncio
import aiohttp

async def fetch_async(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return len(await response.text())

async def fetch_all():
    tasks = [fetch_async(url) for url in urls]
    return await asyncio.gather(*tasks)

start = time.time()
results = asyncio.run(fetch_all())
print(f"Asyncio: {time.time() - start:.2f}s")
# Результат: 2 секунды (no overhead, просто I/O ожидание)

Проблема: Создание каждого процесса требует значительной памяти и времени. Для I/O задач, где большую часть времени процесс просто ждит, это огромное расточительство.

Проблема 2: Разделение памяти между процессами

from multiprocessing import Process, Queue
import requests

def download_file(url, queue):
    # Каждый процесс копирует ВСЮ память главного процесса!
    response = requests.get(url)
    queue.put(response.text)

if __name__ == '__main__':
    q = Queue()
    # Процесс будет содержать копию всех переменных главного процесса
    p = Process(target=download_file, args=('https://example.com', q))
    p.start()
    p.join()
    print(q.get())

Каждый процесс требует отдельного адресного пространства памяти и копии всех переменных. Это дорого!

Проблема 3: GIL не помешает I/O операциям

import threading
import time
import requests

def fetch_url(url):
    print(f"Начало загрузки {url}")
    response = requests.get(url)  # Здесь GIL ОТПУСКАЕТСЯ!
    # (потому что это I/O операция, а не CPU)
    print(f"Конец загрузки {url}")

# ✅ Многопоточность работает идеально для I/O
threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Когда поток выполняет I/O операцию (requests.get), GIL автоматически отпускается, позволяя другим потокам работать. Это означает, что многопоточность идеально подходит для I/O!

Практическое сравнение

Пример 1: Загрузка файлов с сервера

import time
import requests
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import asyncio
import aiohttp

urls = ['https://httpbin.org/delay/2'] * 10  # 10 URLs, 2 сек каждый

# ❌ Многопроцессинг (80+ секунд)
def fetch_sync(url):
    return len(requests.get(url).text)

start = time.time()
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(fetch_sync, urls))
print(f"ProcessPoolExecutor: {time.time() - start:.2f}s")  # ~82 sec

# ✅ Многопоточность (5-6 секунд)
start = time.time()
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch_sync, urls))
print(f"ThreadPoolExecutor: {time.time() - start:.2f}s")  # ~2-3 sec

# ✅✅ Asyncio (2 секунды)
async def main():
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            task = session.get(url)
            tasks.append(task)
        await asyncio.gather(*tasks)

start = time.time()
asyncio.run(main())
print(f"Asyncio: {time.time() - start:.2f}s")  # ~2 sec

Когда всё же использовать multiprocessing для I/O

Сценарий 1: Необходимо обойти GIL для специального кода

import subprocess
import multiprocessing

# Если I/O операция требует CPU (например, парсинг большого HTML)
def parse_and_save(html):
    # CPU-интенсивная парсинг (GIL блокирует)
    parsed = complex_parse(html)
    # I/O сохранение
    save_to_db(parsed)

# Используй multiprocessing, чтобы освободить GIL для парсинга
with ProcessPoolExecutor(max_workers=4) as executor:
    results = executor.map(parse_and_save, html_chunks)

Сценарий 2: Необходимо использовать библиотеку, не выпускающую GIL

# Некоторые библиотеки (старые версии) не выпускают GIL при I/O
# Тогда multiprocessing может помочь
from multiprocessing import Pool

def legacy_io_operation():
    return legacy_library.fetch()  # GIL не выпускается

with Pool(4) as p:
    results = p.map(lambda x: legacy_io_operation(), range(10))

Правильный подход для I/O bound

Вариант 1: asyncio (рекомендуемый)

import asyncio
import aiohttp

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        return await asyncio.gather(*tasks)

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

# Выполнение
results = asyncio.run(fetch_all(urls))

Преимущества:

  • Минимальная память (один поток)
  • Максимальная производительность
  • Просто масштабируется

Вариант 2: ThreadPoolExecutor (для простых случаев)

from concurrent.futures import ThreadPoolExecutor
import requests

def fetch_url(url):
    return requests.get(url).text

with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch_url, urls))

Преимущества:

  • Простая в использовании
  • Хорошо для I/O
  • Не требует переписывания кода в async

Таблица выбора инструмента

Тип задачиInструментПричина
CPU-boundmultiprocessingОбходит GIL, true parallelism
I/O-bound (сетевое)asyncioLowest overhead, высокая пропускная способность
I/O-bound (простое)ThreadPoolExecutorПростое, GIL отпускается на I/O
I/O-bound (блокирующая библиотека)multiprocessingВынужденная мера, но неоптимально
Гибридное (CPU+I/O)asyncio + ProcessPoolExecutorAsyncio для I/O, processes для CPU

Вывод

Можно ли использовать multiprocessing для I/O bound задач?

Технически — можноПрактически — не нужно

Для I/O bound задач используй:

  1. asyncio — если хочешь максимальной производительности
  2. ThreadPoolExecutor — если хочешь простоты
  3. requests + threading — для простых HTTP запросов

Multiprocessing добавит overhead от создания процессов, копирования памяти и IPC, при этом не решив основную проблему (I/O ожидание). GIL выпускается на I/O операциях, поэтому многопоточность естественно хорошо работает для I/O.

Используй правильный инструмент для правильной работы!