Что будет быстрее работать многопоточность (multithreading) или асинхронизация, если нужно у каждого файла уменьшить размер и положить в другую папку?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Многопоточность 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 для файлов
| Аспект | threading | asyncio |
|---|---|---|
| Переключение контекста | Операционная система | 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()
Заключение
Для обработки файлов (уменьшение размера, конвертирование):
- Multiprocessing — ЛУЧШИЙ выбор (истинный параллелизм)
- Threading — хороший выбор (проще чем multiprocessing)
- asyncio — ХУДШИЙ выбор (не поможет для CPU-bound)
Правило: CPU-bound → ProcessPoolExecutor, I/O-bound → asyncio или ThreadPoolExecutor.