Работа с процессами: multiprocessing и параллелизм
Почему процессы? 🏎️💨
Когда твой код упирается в ограничения одного CPU-ядра, а задачи можно выполнять независимо — процессы спешат на помощь! В отличие от потоков (threading), процессы используют несколько ядер CPU и полностью изолированы друг от друга.
Пример-мотивация:
# Обычная синхронная функция
def calculate_squares(numbers):
return [n * n for n in numbers]
# Представь, что numbers содержит 10_000_000 элементов...
Основы multiprocessing 🛠️
Модуль multiprocessing в Python создаёт новые интерпретаторы Python как отдельные процессы. Вот базовый сценарий:
from multiprocessing import Process
import os
def worker(name):
print(f'Процесс {name} (PID: {os.getpid()}) выполняет работу')
if __name__ == '__main__':
processes = []
for i in range(3):
p = Process(target=worker, args=(f'Worker-{i}',))
processes.append(p)
p.start()
for p in processes:
p.join() # Ждём завершения всех процессов
💡 Главное правило — код процессов должен быть внутри if __name__ == '__main__'! Иначе получим рекурсивное создание процессов.
Обмен данными между процессами 📦
Процессы не имеют общей памяти, но есть несколько способов обмена данными:
1. Очереди (Queue)
from multiprocessing import Queue, Process
def producer(q):
q.put('Данные от процесса')
def consumer(q):
print(f'Получено: {q.get()}')
if __name__ == '__main__':
q = Queue()
p1 = Process(target=producer, args=(q,))
p2 = Process(target=consumer, args=(q,))
p1.start(); p2.start()
p1.join(); p2.join()
2. Общая память (Value, Array)
from multiprocessing import Process, Value, Array
def modify(n, arr):
n.value = 3.1415
for i in range(len(arr)):
arr[i] *= 2
if __name__ == '__main__':
num = Value('d', 0.0) # 'd' — double
arr = Array('i', range(5)) # 'i' — integer
p = Process(target=modify, args=(num, arr))
p.start()
p.join()
print(num.value) # 3.1415
print(arr[:]) # [0, 2, 4, 6, 8]
Практический кейс: обработка изображений 🖼️
Допустим, у нас есть папка с изображениями, которые нужно преобразовать:
from multiprocessing import Pool
import cv2, os
def process_image(img_path):
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite(f'processed_{img_path}', gray)
return img_path
if __name__ == '__main__':
images = ['img1.jpg', 'img2.jpg', 'img3.jpg'] # Предположим, файлы существуют
with Pool(processes=os.cpu_count()) as pool:
results = pool.map(process_image, images)
print(f'Обработано: {len(results)} изображений')
🔥 Pool автоматически распределяет задачи по доступным ядрам CPU!
Когда НЕ использовать процессы? ⚠️
- Задачи с общей памятью — процессы не могут легко делиться сложными объектами Python.
- Частая коммуникация — обмен данными между процессами дороже, чем между потоками.
- Очень лёгкие задачи — накладные расходы на создание процесса могут превысить выгоду.
Pro-советы от Данилы Бежина 🧙♂️
- Всегда закрывайте процессы с помощью
join()или контекстного менеджера. - Для CPU-задач процессы почти всегда лучше потоков (GIL не мешает!).
- Используйте
multiprocessing.Poolдля простых сценариев "разделяй и властвуй".
👉 Больше реальных примеров смотри в видео Данилы Бежина!