Deep Engineering

MEASUREMENT

bench/free-threading-races/context_flags.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/python/concurrency/free-threading-races

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Гонки без GIL: замеры для статьи

Проверяется утверждение: код, который в сборке с GIL годами работал правильно, в свободной сборке ломается — хотя гонка в нём была всегда. GIL не делал операции атомарными; он лишь не давал потоку уступить управление в середине короткой последовательности инструкций.

Один и тот же код запускается на трёх интерпретаторах:

сборка что это
3.14.10 обычная сборка, GIL включён
3.14.10t свободная сборка той же версии, GIL выключен
3.15.2t свободная сборка следующей версии: в ней исправлена гонка итератора range (cpython#129068)

Как запустить

uv python install 3.14.10 3.14.10t 3.15.2t
./run.sh        # записывает runs/<сборка>.txt

Нужен компилятор C (cc): gil_reenable.py собирает два крошечных расширения из ext/ под тот интерпретатор, которым запущен.

Скрипт Что меряет
bytecode.py инструкции между чтением и записью в двух формах приращения и есть ли среди них точка, где поток может уступить GIL
lost_update.py сколько приращений из 4 000 000 теряется: c.n += 1, d["n"] += 1, c.n = c.n + one(); два исправления и их цена
check_then_act.py ленивый кеш «проверил — записал»: два отдельных показателя — лишние вызовы make() (extra make() calls) и ключи, для которых потоки получили разные объекты (keys split); setdefault и блокировка. Раскол считается по самим объектам: каждый полученный объект удерживается до конца опыта, id сравниваются после join всех потоков
shared_iterator.py один итератор range или списка на четыре потока: повторы, пропуски, значения вне источника; очередь как исправление
gil_reenable.py импорт расширения многофазной инициализации без слота Py_mod_gil включает GIL для всего процесса: предупреждение и ускорение до и после (однофазный модуль заявляет поддержку вызовом PyUnstable_Module_SetGIL; здесь не меряется)
context_flags.py thread_inherit_context и context_aware_warnings: тот же код даёт другой результат

runs/3.14.10t-old-flags.txt — context_flags.py в свободной сборке с ключами -X thread_inherit_context=0 -X context_aware_warnings=0: так возвращается поведение сборки с GIL.

Машина замеров

2 vCPU (Intel Xeon; модель и частота — во второй строке каждой записи, текущие записи сняты на 2.10GHz), Linux, интерпретаторы из python-build-standalone (uv python install). Числа потерь от прогона к прогону разные — это и есть содержание замера, поэтому печатаются все пять прогонов, а не среднее. Времена — лучшее из пяти.

Script

87 lines
"""Два флага, у которых в свободной сборке другое значение по умолчанию.

What's New in Python 3.14 (раздел Free-threaded mode improvements):

  - sys.flags.thread_inherit_context — поток, созданный threading.Thread,
    стартует с копией контекста (contextvars) того, кто вызвал start();
  - sys.flags.context_aware_warnings — warnings.catch_warnings хранит фильтры
    в переменной контекста, а не меняет общий для процесса список.

Оба флага по умолчанию включены в свободной сборке и выключены в сборке с GIL.
Код ниже — один и тот же; меняется только интерпретатор.

  1. значения флагов;
  2. рабочий поток читает ContextVar и считает Decimal(1) / Decimal(3), а
     запускается внутри decimal.localcontext(prec=6) при request_id = "req-42";
  3. главный поток включает warnings.simplefilter("error") внутри
     catch_warnings, а другой поток в это время вызывает warnings.warn.
"""
import contextvars
import decimal
import sys
import threading
import warnings

from common import env_line, header

request_id = contextvars.ContextVar("request_id", default="(unset)")


def inherited_context():
    out = {}

    def worker():
        out["request_id"] = request_id.get()
        out["third"] = str(decimal.Decimal(1) / decimal.Decimal(3))

    request_id.set("req-42")
    with decimal.localcontext(prec=6):
        t = threading.Thread(target=worker)
        t.start()
        t.join()
    return out


def warnings_across_threads():
    go, done, out = threading.Event(), threading.Event(), {}

    def worker():
        go.wait()
        try:
            warnings.warn("deprecated call", DeprecationWarning)
            out["worker"] = "the warning stayed a warning"
        except DeprecationWarning:
            out["worker"] = "the warning was raised as an exception"
        done.set()

    t = threading.Thread(target=worker)  # создан ДО блока catch_warnings
    t.start()
    with warnings.catch_warnings():
        warnings.simplefilter("error")
        go.set()
        done.wait()
    t.join()
    return out["worker"]


def main():
    print(env_line())

    header(1, "FLAG VALUES")
    print(f"thread_inherit_context = {sys.flags.thread_inherit_context}")
    print(f"context_aware_warnings = {sys.flags.context_aware_warnings}")

    header(2, "CONTEXT IN A NEW THREAD")
    out = inherited_context()
    print(f"request_id in the thread: {out['request_id']}")
    print(f"Decimal(1) / Decimal(3):  {out['third']}")

    header(3, "WARNING FILTER SET IN ANOTHER THREAD")
    # Сам вывод предупреждения в stderr заглушён ключом -W при запуске
    # (run.sh): здесь важно не сообщение, а стало ли оно исключением.
    print(f"in the other thread: {warnings_across_threads()}")


if __name__ == "__main__":
    main()