Как я исправил очень старую ошибку состояния гонки GIL в Python 3.7

задняя часть Python Программа перевода самородков модульный тест

На исправление серьезной ошибки в знаменитой библиотеке Python GIL (Global Interpreter Lock) у меня ушло 4 года., Python GIL — одна из самых подверженных ошибкам частей Python. Мне пришлось покопаться в истории коммитов Git, чтобы найти 26 лет назад.Guido van RossumПредставленная запись: В то время,Темы все еще очень неясная вещь. И слушай меня медленно.

Фатальная ошибка Python, вызванная потоком C и GIL

В марте 2014 г.Steve DowerСообщается об ошибке, когда «поток C» использует Python C API.bpo-20891:

В Python 3.4rc3 вызывается в потоке, не созданном в Python.PyGILState_Ensure()метод, но не вызываетPyEval_InitThreads()метод, это приведет к сбою программы с фатальной ошибкой:

Fatal Python error: take_gil: NULL tstate

Мой первый комментарий:

по моему этоPyEval_InitThreads()Жук.

Release the GIL!

Исправление PyGILState_Ensure()

Я забыл об этой ошибке в течение двух лет. К марту 2016 года я изменил тестовый код Стива, чтобы он был совместим с Linux (в то время тестовый код был написан для Windows). Я успешно воспроизвел эту ошибку на своем компьютере и написалPyGILState_Ensure()патч для ремонта.

Через год, в ноябре 2017 г.Marcin Kasperskiспросил:

Это исправление уже выпущено? Я не вижу его в журнале изменений...

Ой, опять я совсем забыл про этот вопрос! На этот раз я не толькоОтправлено мое исправление для PyGILState_Ensure(), тоже писалмодульный тест test_embed.test_bpo20891():

Что ж, эта ошибка была исправлена ​​в Python 2.7, 3.6 и основной ветке (позднее 3.7). В версии 3.6 и основной версии этот патч также поставляется с модульными тестами.

мое исправление фиксируется в основной ветке, фиксируетсяb4d1e1f7:

bpo-20891: Fix PyGILState_Ensure() (#4650)

When PyGILState_Ensure() is called in a non-Python thread before
PyEval_InitThreads(), only call PyEval_InitThreads() after calling
PyThreadState_New() to fix a crash.

Add an unit test in test_embed.

Тогда я закрыл эту темуbpo-20891в настоящее время...

Модульные тесты случайным образом аварийно завершают работу в macOS

Все было хорошо... пока через неделю я не понял, что мой недавно добавленный модульный тест был на macOS.временамирухнет. В конце концов мне удалось найти путь для воспроизведения, следующий пример вылетает при третьем запуске:

macbook:master haypo$ while true; do ./Programs/_testembed bpo20891 ||break; date; done
Lun  4 déc 2017 12:46:34 CET
Lun  4 déc 2017 12:46:34 CET
Lun  4 déc 2017 12:46:34 CET
Fatal Python error: PyEval_SaveThread: NULL tstate

Current thread 0x00007fffa5dff3c0 (most recent call first):
Abort trap: 6

test_embed.test_bpo20891()на macOSPyGILState_Ensure()Возникло состояние гонки: сам замок GIL сконструирован... никакой защиты замка! Очевидно, что добавление блокировки для определения того, есть ли в настоящее время в Python блокировка GIL, бессмысленно...

Я предлагаю исправитьPyThread_start_new_thread()Не очень полное предложение:

Я нашел работоспособное исправление: вPyThread_start_new_thread()вызыватьPyEval_InitThreads(). Таким образом, GIL может быть создан, как только будет создан второй поток. GIL больше не может быть создан при наличии двух запущенных потоков. Но, по крайней мере, в "Разве этоpython«В этой черно-белой ситуации, если поток не был создан в Python, исправление не удастся, но тогда поток вызоветPyGILState_Ensure().

Почему бы не создать GIL в первую очередь?

Antoine PitrouЗадал простой вопрос:

почему нетКогда парсер инициализируетсяпросто позвониPyEval_InitThreads()? Есть ли недостаток?

благодаряgit blameиgit logкоманда, я нашел источник кода "создать GIL по запросу",Изменение по сравнению с 26-летней давностью!

commit 1984f1e1c6306d4e8073c28d2395638f80ea509b
Author: Guido van Rossum <guido@python.org>
Date:   Tue Aug 4 12:41:02 1992 +0000

    * Makefile adapted to changes below.
    * split pythonmain.c in two: most stuff goes to pythonrun.c, in the library.
    * new optional built-in threadmodule.c, build upon Sjoerd's thread.{c,h}.
    * new module from Sjoerd: mmmodule.c (dynamically loaded).
    * new module from Sjoerd: sv (svgen.py, svmodule.c.proto).
    * new files thread.{c,h} (from Sjoerd).
    * new xxmodule.c (example only).
    * myselect.h: bzero -> memset
    * select.c: bzero -> memset; removed global variable

(...)

+void
+init_save_thread()
+{
+#ifdef USE_THREAD
+       if (interpreter_lock)
+               fatal("2nd call to init_save_thread");
+       interpreter_lock = allocate_lock();
+       acquire_lock(interpreter_lock, 1);
+#endif
+}
+#endif

Я предполагаю, что цель этого динамического создания GIL состоит в том, чтобы избежать «преждевременного» создания GIL приложениями, которые используют только один поток (т. е. никогда не создают новый поток).

К счастью,Guido van RossumБыл там в то время и смог работать со мной, чтобы выяснить основную причину:

Да, первоначальная причина былаПотоки очень неясны, и не так много кода, который использует потоки., то из-за багов в коде GIL мы бы точно почувствовалиЧастое использование GIL приводит к (незначительному) снижению производительности.иРастущий риск аварии. Теперь, когда мы знаем, что нам больше не нужно беспокоиться об этих двух аспектах, мы можемНе стесняйтесь инициализировать его с помощью.

Предлагаемое второе исправление для Py_Initialize()

я предложилPy_Initialize()изЕще одно исправление: Всегда создавайте GIL сразу после запуска Python, а не «по запросу», чтобы избежать риска возникновения условий гонки:

+    /* Create the GIL */
+    PyEval_InitThreads();

Nick CoghlanСпросите меня, могу ли я запустить тест производительности на моем патче. я в своемPR 4700запускать наpyperformance, разница до 5%:

haypo@speed-python$ python3 -m perf compare_to \
    2017-12-18_12-29-master-bd6ec4d79e85.json.gz \
    2017-12-18_12-29-master-bd6ec4d79e85-patch-4700.json.gz \
    --table --min-speed=5

+----------------------+--------------------------------------+-------------------------------------------------+
| Benchmark            | 2017-12-18_12-29-master-bd6ec4d79e85 | 2017-12-18_12-29-master-bd6ec4d79e85-patch-4700 |
+======================+======================================+=================================================+
| pathlib              | 41.8 ms                              | 44.3 ms: 1.06x slower (+6%)                     |
+----------------------+--------------------------------------+-------------------------------------------------+
| scimark_monte_carlo  | 197 ms                               | 210 ms: 1.07x slower (+7%)                      |
+----------------------+--------------------------------------+-------------------------------------------------+
| spectral_norm        | 243 ms                               | 269 ms: 1.11x slower (+11%)                     |
+----------------------+--------------------------------------+-------------------------------------------------+
| sqlite_synth         | 7.30 us                              | 8.13 us: 1.11x slower (+11%)                    |
+----------------------+--------------------------------------+-------------------------------------------------+
| unpickle_pure_python | 707 us                               | 796 us: 1.13x slower (+13%)                     |
+----------------------+--------------------------------------+-------------------------------------------------+

Not significant (55): 2to3; chameleon; chaos; (...)

Ничего себе, 5 тестов вниз. Регрессионное тестирование производительности популярно в Python: мы работаем над этимЗаставьте Python работать быстрее!

Пропустить неудачные тесты в канун Рождества

Я не ожидал, что 5 тестов снизят производительность. Это требует более глубокого изучения, но у меня нет на это времени, и я слишком застенчив/стыжусь, чтобы нести ответственность за регрессионное тестирование производительности.

Однако я не могу принять решение до рождественских каникул.test_embed.test_bpo20891()Все еще случайный сбой на macOS, как всегда. То, что я добрался до наиболее подверженной ошибкам части Python — GIL, сильно ударило по мне за две недели до праздников. Поэтому я решил пропуститьtest_bpo20891()модульное тестирование до окончания праздников.

Python 3.7, никаких пасхалок.

Sad Christmas tree

Запускаем новый бенчмарк, второе исправление слито в ветку master

В конце января 2018 года я снова провел 5 тестов, где производительность ухудшилась в моем PR. Я запускаю эти тесты вручную на своем ноутбуке, используя разные процессоры для разных тестов:

vstinner@apu$ python3 -m perf compare_to ref.json patch.json --table
Not significant (5): unpickle_pure_python; sqlite_synth; spectral_norm; pathlib; scimark_monte_carlo

Ну, согласноНабор тестов производительности Python, теперь доказывая, что мое второе исправление на самом деле неНе сильно влияет на производительность.

Я решил отправить исправление в ветку master, зафиксировать2914bb32:

bpo-20891: Py_Initialize() now creates the GIL (#4700)

The GIL is no longer created "on demand" to fix a race condition when
PyGILState_Ensure() is called in a non-Python thread.

Затем я перезапустил основную веткуtest_embed.test_bpo20891()модульный тест.

К сожалению, второго исправления для Python 2.7 и 3.6 нет!

Antoine PitrouЗадумался о переносе патча на Python 3.6нельзя объединить:

Я не думаю, что это необходимо. Каждый может позвонитьPyEval_InitThreads().

Guido van RossumТакже не хочу портировать этот патч. Поэтому я удалил его из основной ветки 3.6.test_embed.test_bpo20891().

По этой же причине я тоже не стал применять свой второй патч в Python 2.7, к тому же в Python 2.7 нет юнит-тестов, потому что портирование слишком сложное.

Но, по крайней мере, Python 2.7 и 3.6 применили мой первый патч,PyGILState_Ensure().

Суммировать

Python по-прежнему имеет некоторые условия гонки в некоторых крайних случаях. Эта ошибка была обнаружена, когда поток C создал GIL с помощью Python API. Я нажал первый патч, но в macOS появилось еще одно новое состояние гонки.

Мне пришлось копаться в очень старой истории коммитов (1992 г.) Python GIL. к счастьюGuido van RossumМожет помочь найти основную причину ошибки вместе.

После сбоя в тесте мы согласились, что в Python 3.7 GIL всегда создается сразу после запуска парсера, а не «по требованию». Это изменение не оказало заметного влияния на производительность.

Мы также решили оставить Python 2.7 и 3.6 без изменений, чтобы предотвратить любой риск регрессионного тестирования: продолжайте создавать GIL «по запросу».

На исправление серьезной ошибки в знаменитой библиотеке Python GIL (Global Interpreter Lock) у меня ушло 4 года., Python GIL — одна из самых подверженных ошибкам частей Python. Я рад, что эта ошибка устранена: она полностью исправлена ​​в грядущем выпуске Python 3.7!

существуетbpo-20891Посмотреть полную историю. Спасибо всем разработчикам, которые помогли мне исправить эту ошибку!


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.