Ответ
Выбор зависит от характера задачи, а именно от того, является ли она CPU-bound (ограничена производительностью процессора) или I/O-bound (ограничена скоростью ввода-вывода).
Задача по обработке изображений состоит из двух частей:
- Чтение/запись файлов: I/O-bound операция.
- Изменение размера изображения: 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.
Ответ 18+ 🔞
Давай разжую эту тему, как последнюю котлету в столовой, а то народ путается в трёх соснах — асинхронность, потоки, процессы. Суть в чём, блядь? Всё упирается в то, чем твоя задача ограничена.
Представь, ты — шеф-повар на кухне:
- CPU-bound задача — это когда ты сам, один, долго и нудно месишь тесто в одной тазике. Добавь хоть десять помощников, которые будут стоять и смотреть — быстрее не замесишь. Ограничение — твои собственные руки (процессор).
- I/O-bound задача — это когда ты отправил десять заказов на кухню и ждёшь, пока их приготовят. Ты мог бы тупо стоять и ждать каждый, но гораздо эффективнее отдать все заказы и заниматься своим делом, пока они готовятся. Ограничение — скорость работы кухни (ввод-вывод).
Теперь про нашу конкретную задачу — обработка изображений. Она, сука, гибридная, как мартышка с гранатой:
- Прочитать файл с диска, записать на диск — это чистой воды I/O. Тут мы ждём, пока жёсткий диск или SSD соизволят нам ответить.
- Изменить размер, применить фильтры — это уже CPU-bound. Тут процессор должен попотеть, пересчитывая пиксели.
А теперь главный гад в нашей истории — GIL (Global Interpreter Lock) в CPython. Это такой смотритель в тюрьме, который в любой момент времени пропускает на волю только одного зека (один поток на выполнение Python-кода). Остальные сидят и ждут своей очереди, даже если у тебя ядер, как семечек в тыкве.
И вот как наши инструменты с этим GIL'ом взаимодействуют:
-
Многопоточность (
threading): Это как если бы ты нанял десять поваров (потоков), но на кухне одна-единственная скалка (GIL). Пока один месит тесто, остальные девять тупо курят в сторонке. Для I/O-задач — ок, они могут курить, пока ждут ответа от диска. Но для CPU-задач — полный пиздец и профанация, ускорения ноль целых хуй десятых. -
Асинхронность (
asyncio): Это когда ты один повар-супермен, который научился молниеносно переключаться между задачами. Поставил картошку вариться — побежал резать салат, вернулся — картошка готова. Идеально, когда задач дохуища и все они про ожидание (сеть, диск). Но если тебе надо замесить десять кг теста (CPU-работа), то переключаться не на что — ты просто долго и упорото месишь. Для нашей задачи с картинками — не катит. -
Мультипроцессинг (
multiprocessing): А вот это, блядь, ядерный вариант! Ты строишь несколько отдельных кухонь (процессов), в каждой — свой повар, своё оборудование и, что главное, своя отдельная скалка (свой GIL в каждом процессе). Теперь десять поваров могут месить десять кусков теста одновременно, по-настоящему. Да, между кухнями надо как-то сообщаться (это накладные расходы), но для тяжёлых вычислений — это единственный путь в CPython.
Вывод, ёпта: Раз основная тяжесть в нашей задаче — это пересчёт пикселей (CPU-bound), то мультипроцессинг — наш бронепоезд. Он позволит загрузить все ядра процессора по полной, как следует.
Вот тебе рабочий пример, чтоб не быть голословным:
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, других вариантов нет, в рот меня чих-пых!