Казалось бы, Python 3.13+ без GIL должен позволить нескольким потокам одновременно выполнять численные операции.
Но на практике параллельные
ufunc начинали конкурировать за внутренние структуры NumPy, счётчики ссылок и выделение памяти.В NumPy 2.5 разработчики устранили несколько ключевых узких мест:
- таблицу диспетчеризации
ufunc заменили на lock-free concurrent hash map;- часть общих служебных объектов сделали «бессмертными», снизив конкуренцию при подсчёте ссылок;
- выделение памяти перевели на
PyMem_RawMalloc и PyMem_RawFree.Теперь независимые операции можно масштабировать обычными потоками:
from concurrent.futures import ThreadPoolExecutor
import numpy as np
arrays = [
np.random.rand(5_000_000)
for _ in range(8)
]
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(np.sin, arrays))
Главное преимущество перед
multiprocessing - массивы не нужно сериализовать и копировать между процессами.Потоки работают в одном адресном пространстве, поэтому могут эффективнее использовать многоядерный CPU и общую память.
Но есть важная оговорка:
free-threaded не означает автоматически thread-safe.
Одновременное изменение одного
ndarray из нескольких потоков всё ещё может привести к гонкам, повреждённым данным и падению интерпретатора.Безопаснее использовать:
- отдельный массив для каждого worker;
- общий массив только для чтения;
- явную синхронизацию при записи.
Удалить GIL оказалось недостаточно. Чтобы Python действительно масштабировался по ядрам, пришлось перепроектировать и внутренности NumPy.
Подробнее:
https://labs.quansight.org/blog/scaling-numpy-on-free-threaded-python
