📚Python Books
Фотография
🐍 Free-threaded Python убрал GIL, но NumPy всё равно упирался во внутренние блокировки
Казалось бы, 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
12 · 6K ·