Что выбрать в Python для пакетной обработки файлов: многопоточность, мультипроцессинг или асинхронность?

Ответ

Выбор зависит от характера задачи, а именно от того, является ли она CPU-bound (ограничена производительностью процессора) или I/O-bound (ограничена скоростью ввода-вывода).

Задача по обработке изображений состоит из двух частей:

  1. Чтение/запись файлов: I/O-bound операция.
  2. Изменение размера изображения: CPU-bound операция.

Проанализируем подходы с учётом GIL (Global Interpreter Lock) в CPython, который не позволяет нескольким потокам выполнять Python-байткод одновременно на разных ядрах CPU.

  • Многопоточность (threading): Эффективна для I/O-bound задач, так как потоки могут переключаться во время ожидания операций ввода-вывода. Однако из-за GIL она не ускоряет CPU-bound вычисления.
  • Асинхронность (asyncio): Идеальна для большого количества I/O-операций (например, сетевых запросов). Работает в одном потоке, поэтому не подходит для CPU-bound задач.
  • Мультипроцессинг (multiprocessing): Создаёт отдельные процессы, каждый со своим интерпретатором Python и GIL. Это единственный способ в CPython по-настоящему распараллелить CPU-bound задачи на несколько ядер.

Вывод: Поскольку изменение размера изображений — это ресурсоёмкая CPU-bound операция, мультипроцессинг является лучшим выбором для этой задачи. Он позволит эффективно использовать все ядра процессора.

Пример с ProcessPoolExecutor:

from concurrent.futures import ProcessPoolExecutor
import os
from PIL import Image

INPUT_DIR = 'input_images'
OUTPUT_DIR = 'output_images'

def resize_image(filename):
    """Читает изображение, уменьшает его и сохраняет в другую папку."""
    try:
        input_path = os.path.join(INPUT_DIR, filename)
        output_path = os.path.join(OUTPUT_DIR, filename)

        with Image.open(input_path) as img:
            img.thumbnail((800, 800)) # Уменьшаем до 800x800
            img.save(output_path)
        return f'{filename} успешно обработан.'
    except Exception as e:
        return f'Ошибка при обработке {filename}: {e}'

if __name__ == '__main__':
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    files = [f for f in os.listdir(INPUT_DIR) if f.endswith(('jpg', 'png'))]

    # Используем пул процессов для параллельной обработки
    with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
        results = executor.map(resize_image, files)
        for result in results:
            print(result)

Краткое правило:

  • I/O-bound (сеть, диск): asyncio или threading.
  • CPU-bound (вычисления, обработка данных): multiprocessing.