GIL
GIL — глобальная блокировка интерпретатора CPython: в каждый момент байткод Python выполняет только один поток. Следствия:
- задачи ввода-вывода потоки ускоряют — на ожидании сети и диска GIL отпускается;
- вычисления потоки не ускоряют — для них процессы или C-расширения, отпускающие GIL (NumPy).
Три инструмента
| Что это | Для чего | |
|---|---|---|
| threading | потоки одного процесса, общая память | ожидание ввода-вывода |
| multiprocessing | отдельные процессы со своей памятью и своим GIL | вычисления на нескольких ядрах |
| asyncio | один поток, кооперативная многозадачность | тысячи одновременных соединений |
Блокировки всё равно нужны
GIL защищает внутренние структуры интерпретатора, а не ваши инварианты. counter += 1 — несколько инструкций байткода: прочитать, сложить, записать. Поток может быть прерван между ними, и обновления теряются.
import threading
lock = threading.Lock()
def inc():
global counter
with lock:
counter += 1
«Проверить и изменить» (if k not in d: d[k] = ...) — тоже гонка. Лучший способ — не делить изменяемое состояние: передавать данные через queue.Queue и использовать неизменяемые структуры.
concurrent.futures
Единый интерфейс для пулов потоков и процессов:
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=16) as pool:
futures = [pool.submit(download, url) for url in urls]
for f in as_completed(futures):
save(f.result()) # исключение задачи бросится здесь
ThreadPoolExecutor — для ввода-вывода, ProcessPoolExecutor — для вычислений; интерфейс одинаковый. with гарантирует остановку пула и ожидание задач.
Ограничения процессов
Процессы не делят память: аргументы и результаты сериализуются pickle и копируются. Поэтому:
- лямбды, локальные функции, открытые файлы и соединения передать нельзя;
- большие данные гонять дорого — лучше передать путь к файлу или использовать
shared_memory.
Способ запуска важен. fork копирует процесс родителя — быстро, но опасно, если в родителе уже работали потоки: можно унаследовать захваченную блокировку и зависнуть. spawn запускает чистый интерпретатор и заново импортирует модуль — поэтому нужен if __name__ == "__main__":. В macOS и Windows по умолчанию spawn, в Linux с Python 3.14 — forkserver.
Python без GIL
С Python 3.13 есть отдельная сборка без GIL (python3.13t), в 3.14 она официально поддерживается, но не включена по умолчанию. Потоки выполняют Python-код по-настоящему параллельно, вычисления в потоках ускоряются почти линейно по ядрам. Цена — медленнее однопоточный код, а C-расширения должны объявить совместимость, иначе GIL включится снова. Гонки в вашем коде никуда не денутся — их станет только больше.
Что запомнить
- GIL мешает вычислениям в потоках, но не вводу-выводу.
- GIL не защищает ваши данные — нужны Lock или очереди.
- Вычисления — процессы, ожидание — потоки или asyncio.
- Процессам всё передаётся через pickle.