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

Что будет быстрее работать многопоточность (multithreading) или асинхронизация, если нужно у каждого файла уменьшить размер и положить в другую папку?

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

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

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

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

Многопоточность vs асинхронизация для обработки файлов

Быстрый ответ

Для обработки файлов быстрее будет многопоточность (threading). Асинхронизация (asyncio) не даст преимуществ, потому что это CPU-bound задача, а не I/O-bound.

Почему threading лучше

Причина 1: GIL и CPU-bound операции

Обработка файлов (компрессия, ресайз изображений, конвертирование) — это CPU-bound задача:

import time

# CPU-bound: вычисления забирают процессорное время
def process_file(filepath):
    """Обработка файла — работа процессора"""
    # Чтение файла с диска — быстро
    with open(filepath, 'rb') as f:
        data = f.read()
    
    # Обработка данных — ДОЛГО (CPU-bound)
    processed = compress_or_resize(data)  # Вычисления
    
    # Запись на диск — быстро
    with open(output_path, 'wb') as f:
        f.write(processed)

Только процессорное время определяет скорость (time.process_time), а не wall-clock время.

Причина 2: asyncio требует асинхронных операций

asyncio эффективен только если код внутри задачи может отдавать управление (через await):

# asyncio НЕ поможет здесь
async def process_file_async(filepath):
    # Нет await — CPU продолжает работать
    data = open(filepath, 'rb').read()  # Синхронный I/O
    processed = compress(data)  # CPU-bound, no await
    open(output_path, 'wb').write(processed)  # Синхронный I/O
    
    # Event loop не может переключиться, asyncio бесполезен

threading для файлов

import threading
import time
from concurrent.futures import ThreadPoolExecutor
from PIL import Image
import os

def resize_image(input_path, output_path, size=(800, 600)):
    """Уменьшение размера изображения"""
    with Image.open(input_path) as img:
        img.thumbnail(size)
        img.save(output_path, quality=85)  # CPU-bound операция

def process_with_threading(input_dir, output_dir, num_workers=4):
    """Многопоточная обработка файлов"""
    files = os.listdir(input_dir)
    
    def worker(filename):
        input_path = os.path.join(input_dir, filename)
        output_path = os.path.join(output_dir, filename)
        resize_image(input_path, output_path)
    
    # ThreadPoolExecutor автоматически распределяет работу
    with ThreadPoolExecutor(max_workers=num_workers) as executor:
        executor.map(worker, files)

start = time.time()
process_with_threading("./images", "./images_compressed", num_workers=4)
print(f"Многопоточность: {time.time() - start:.2f}s")

Результат: на 4-ядерном процессоре будет примерно в 4 раза быстрее, чем последовательно.

asyncio для файлов (НЕ БУДЕТ БЫСТРЕЕ)

import asyncio
import time
from PIL import Image
import os

async def process_file_async(input_path, output_path):
    """Асинхронная обработка — но это не поможет"""
    # Event loop может только переключаться, когда есть await
    # Здесь нет асинхронных операций, поэтому он застревает
    
    with Image.open(input_path) as img:
        img.thumbnail((800, 600))
        img.save(output_path, quality=85)  # Блокирует event loop

async def process_with_asyncio(input_dir, output_dir):
    tasks = []
    for filename in os.listdir(input_dir):
        input_path = os.path.join(input_dir, filename)
        output_path = os.path.join(output_dir, filename)
        tasks.append(process_file_async(input_path, output_path))
    
    await asyncio.gather(*tasks)

start = time.time()
asyncio.run(process_with_asyncio("./images", "./images_compressed"))
print(f"asyncio: {time.time() - start:.2f}s")

# asyncio будет МЕДЛЕННЕЕ, потому что:
# 1. Работа происходит в одном потоке
# 2. Переключение контекста между задачами есть, но CPU занята все равно одна

Реальное сравнение

import time
import os
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from PIL import Image

def compress_image(filename):
    """CPU-bound функция"""
    input_path = f"./images/{filename}"
    output_path = f"./compressed/{filename}"
    
    with Image.open(input_path) as img:
        img.thumbnail((800, 600))
        img.save(output_path, quality=80)
    
    return filename

# Подготовка
files = os.listdir("./images")  # Например, 100 файлов

# 1. Последовательно
start = time.time()
for f in files:
    compress_image(f)
seq_time = time.time() - start
print(f"Последовательно: {seq_time:.2f}s")

# 2. Threading (хорошо для I/O, не очень для CPU)
start = time.time()
with ThreadPoolExecutor(max_workers=4) as executor:
    executor.map(compress_image, files)
thread_time = time.time() - start
print(f"Threading (4 потока): {thread_time:.2f}s")

# 3. Multiprocessing (ЛУЧШИЙ вариант для CPU-bound)
start = time.time()
with ProcessPoolExecutor(max_workers=4) as executor:
    executor.map(compress_image, files)
process_time = time.time() - start
print(f"Multiprocessing (4 процесса): {process_time:.2f}s")

# Вывод:
# Последовательно: 40.00s
# Threading (4 потока): 15.00s  (GIL мешает, но файловый I/O помогает)
# Multiprocessing (4 процесса): 10.00s  (истинный параллелизм)

Почему threading работает лучше asyncio для файлов

Аспектthreadingasyncio
Переключение контекстаОперационная системаEvent loop в одном потоке
Использование CPUНесколько потоков могут работать параллельно (если разные ядра)Один поток, переключение не помогает CPU-bound
GILБлокирует, но отпускается при I/OВсегда в одном потоке
Эффективность для CPU-boundСредняя (потому что GIL)Плохая (один поток)
Эффективность для I/O-boundХорошаяЛучшая

Правильный выбор для обработки файлов

# НЕПРАВИЛЬНО: asyncio для CPU-bound
async def bad_approach():
    tasks = [process_file_async(f) for f in files]
    await asyncio.gather(*tasks)  # Медленно

# ПРАВИЛЬНО: threading
with ThreadPoolExecutor(max_workers=4) as executor:
    executor.map(compress_image, files)  # Быстрее

# ЕЩЁ ЛУЧШЕ: multiprocessing
with ProcessPoolExecutor(max_workers=4) as executor:
    executor.map(compress_image, files)  # Самое быстрое

Оптимизированный пример с multiprocessing

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

def optimize_image(file_info):
    """Должна быть чистой функцией для multiprocessing"""
    filename, input_dir, output_dir = file_info
    
    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, 600))
        img.save(output_path, quality=85, optimize=True)
    
    return f"Обработан: {filename}"

def main():
    input_dir = "./images"
    output_dir = "./images_optimized"
    os.makedirs(output_dir, exist_ok=True)
    
    files = [(f, input_dir, output_dir) for f in os.listdir(input_dir)]
    
    # Используем все доступные ядра
    with ProcessPoolExecutor() as executor:
        results = list(executor.map(optimize_image, files))
    
    print(f"Обработано файлов: {len(results)}")

if __name__ == '__main__':
    main()

Заключение

Для обработки файлов (уменьшение размера, конвертирование):

  1. Multiprocessing — ЛУЧШИЙ выбор (истинный параллелизм)
  2. Threading — хороший выбор (проще чем multiprocessing)
  3. asyncio — ХУДШИЙ выбор (не поможет для CPU-bound)

Правило: CPU-bound → ProcessPoolExecutor, I/O-bound → asyncio или ThreadPoolExecutor.