Как в Python эффективно обрабатывать CPU-bound задачи

Ответ

CPU-bound задачи (интенсивные вычисления) в стандартном CPython упираются в Global Interpreter Lock (GIL), который не позволяет нескольким потокам выполнять Python-байткод одновременно на разных ядрах CPU. Для обхода этого ограничения используются следующие подходы:

  1. Многопроцессорность (multiprocessing) Самый распространенный способ. Каждый процесс получает собственный интерпретатор Python и, соответственно, собственный GIL, что позволяет выполнять код параллельно на разных ядрах. Почему: Простота использования и наличие в стандартной библиотеке.

    from multiprocessing import Pool
    
    def square(x):
        return x * x
    
    if __name__ == '__main__':
        # Создаем пул из 4 процессов
        with Pool(4) as p:
            results = p.map(square, range(100))
            print(results)
  2. concurrent.futures.ProcessPoolExecutor Более высокоуровневый интерфейс для работы с пулом процессов, построенный поверх multiprocessing. Почему: Удобный и современный API для управления асинхронными задачами.

    from concurrent.futures import ProcessPoolExecutor
    
    def square(x):
        return x * x
    
    with ProcessPoolExecutor(max_workers=4) as executor:
        results = list(executor.map(square, range(100)))
        print(results)
  3. Numba JIT (Just-In-Time) компилятор, который переводит числовой Python-код в быстрый машинный код, часто обходя GIL. Почему: Идеально для научных и математических вычислений, особенно с использованием NumPy. Требует минимальных изменений в коде (только декоратор).

    from numba import jit
    import numpy as np
    
    @jit(nopython=True) # nopython=True гарантирует отсутствие вызовов Python API
    def sum_array(arr):
        total = 0.0
        for i in range(arr.shape[0]):
            total += arr[i]
        return total
    
    my_array = np.arange(1_000_000, dtype=np.float64)
    sum_array(my_array) # Первый вызов компилирует функцию
  4. Cython Позволяет писать код на диалекте Python с C-аннотациями типов, который затем компилируется в нативный C/C++ код. Позволяет вручную освобождать GIL для параллельного выполнения в потоках. Почему: Дает максимальный контроль над производительностью и взаимодействием с C-библиотеками.

Вывод: для большинства общих задач multiprocessing является лучшим выбором. Для числовых вычислений стоит предпочесть Numba, а для максимальной оптимизации и интеграции с C — Cython.