Python C API — это потокобезопасно?

У меня есть расширение C, которое вызывается из моего многопоточного приложения Python. Я использую статическую переменную i где-то в функции C, и позже у меня есть несколько операторов i++, которые можно запускать из разных потоков Python (хотя эта переменная используется только в моем коде C, я не уступаю ее Python) .

По какой-то причине я до сих пор не встретил ни одного условия гонки, но мне интересно, может быть, это просто удача...

У меня нет кода C, связанного с потоками (нет Py_BEGIN_ALLOW_THREADS или чего-то еще).

Я знаю, что GIL гарантирует атомарность и потокобезопасность только инструкций с одним байт-кодом, поэтому операторы типа i+=1 в Python не являются потокобезопасными.

Но я не знаю об инструкции i++ в расширении C. Любая помощь ?


person DenverCoder9    schedule 02.02.2017    source источник
comment
Я знаю, что GIL гарантирует атомарность и поточно-ориентированность только инструкций с одним байт-кодом — он даже этого не гарантирует. Однако ваш i++ в C должен быть в порядке; GIL не может быть выпущен посреди этого. Код C не освободит GIL, если он не сделает явный вызов, чтобы дать другим потокам возможность запускаться (но будьте осторожны с вызовами кода, который вы не контролируете, который может сделать этот вызов для вас).   -  person user2357112 supports Monica    schedule 02.02.2017
comment
Вау, теперь я еще больше запутался. Я прочитал здесь, что инструкции с одним байт-кодом являются потокобезопасными... И что вы имеете в виду, что код C никогда не выпустит GIL, если это явно не указано? Например, даже если я поставлю sleep или какую-нибудь инструкцию ожидания/ввода? Как только вы вводите код C, это всего лишь одно атомарное выполнение?   -  person DenverCoder9    schedule 03.02.2017
comment
Распространенное заблуждение, но нет, они не потокобезопасны, наиболее очевидно, потому что один код операции BINARY_ADD или любой другой код операции может преобразовать в произвольную определяемую пользователем функцию, написанную на Python. Вы должны быть уверены, что выполнение кода операции не может привести к вызову другого кода Python и что любой задействованный код C не приведет к явному освобождению GIL.   -  person user2357112 supports Monica    schedule 03.02.2017
comment
Очень хороший момент, я не знал об этом, спасибо!   -  person DenverCoder9    schedule 03.02.2017


Ответы (1)


Python не выпускает GIL, когда вы запускаете код C (если только вы не сообщите об этом или не вызовете выполнение кода Python — см. предупреждение внизу!). Он освобождает GIL только непосредственно перед инструкцией байт-кода (не во время), и с точки зрения интерпретатора выполнение функции C является частью выполнения CALL_FUNCTION байт-кода.* (к сожалению, я не могу найти ссылку на этот абзац в настоящее время , но я почти уверен, что это правильно)

Поэтому, если вы не сделаете что-то конкретное, ваш код C будет единственным запущенным потоком, и поэтому любая операция, которую вы выполняете в нем, должна быть потокобезопасной.

Если вы специально хотите выпустить GIL — например, потому что вы делаете длинные вычисления, которые не мешают Python, читаете из файла или спите, ожидая, что что-то еще произойдет — тогда самый простой способ — это сделать < a href="https://docs.python.org/3/c-api/init.html#releasing-the-gil-from-extension-code" rel="nofollow noreferrer">Py_BEGIN_ALLOW_THREADS, затем Py_END_ALLOW_THREADS, когда вы хотите вернуть его. Во время этого блока вы не можете использовать большинство функций API Python, и вы несете ответственность за обеспечение безопасности потоков в C. Самый простой способ сделать это — использовать только локальные переменные, а не читать или записывать какое-либо глобальное состояние.

Если у вас уже есть поток C, работающий без GIL (поток A), то простое сохранение GIL в потоке B не гарантирует, что поток A не будет изменять глобальные переменные C. Чтобы быть в безопасности, вы должны убедиться, что вы никогда не изменяете глобальное состояние без какого-либо механизма блокировки (будь то Python GIL или механизм C) во всех ваших функциях C.


Дополнительное мнение

* Одно из мест, где GIL может быть выпущен в коде C, — это если код C вызывает что-то, что вызывает выполнение кода Python. Это может быть связано с использованием PyObject_Call. Менее очевидное место было бы, если бы Py_DECREF вызвало выполнение деструктора. Вы бы вернули GIL к тому времени, когда ваш код C возобновился, но вы больше не могли гарантировать, что глобальные объекты не изменятся. Это очевидное не влияет на простой C, такой как x++.


Запоздалое редактирование:

Следует подчеркнуть, что очень, очень, очень легко вызвать выполнение кода Python. По этой причине вы не должны использовать GIL вместо мьютекса или фактического механизма блокировки. Вы должны рассматривать его только для операций, которые действительно являются атомарными (т. е. один вызов API C) или полностью для объектов C, отличных от Python. Вы не потеряете GIL неожиданно при выполнении C-кода, но многие вызовы C API могут освободить GIL, сделать что-то еще, а затем восстановить GIL, прежде чем вернуться к вашему C-коду.

Цель GIL — убедиться, что внутренние компоненты Python не повреждены. GIL будет продолжать служить этой цели в модуле расширения. Однако условия гонки, включающие действительные объекты Python, расположенные неожиданным для вас образом, по-прежнему доступны для вас. Например:

PySequence_SetItem(some_list, 0, some_item);
PyObject* item = PySequence_GetItem(some_list, 0);
assert(item == some_item); // may not be true 
// the destructor of the previous contents of item 0 may have released the GIL
person DavidW    schedule 03.02.2017
comment
Это довольно много, я не знал, что с точки зрения интерпретатора вызовы функций расширений C были атомарными. Это означает, что если вы намерены провести какое-то время в своем расширении C, вы должны явно сообщить своему коду о выпуске GIL, иначе вы даже не сможете позволить другим потокам выполнять вызовы, ожидающие завершения ввода-вывода? Спасибо за понимание в любом случае. Есть ли документация по этому поводу? Я ничего не нашел. - person DenverCoder9; 03.02.2017
comment
Логика заключается в том, что вам нужно удерживать GIL для использования любого вызова API Python (часто возникает ошибка, если вы этого не делаете). Если бы нужно было просто выпустить сам GIL, то никогда нельзя было бы быть уверенным, что что-то безопасно. Поэтому он зависит от вашего собственного суждения о том, когда вам не нужен GIL. Пока у вас есть GIL, ничего нового не запустится, но если другие потоки уже ожидают завершения ввода-вывода, они будут продолжать делать это в фоновом режиме, пока работает ваша функция C (но они не будут ничего делать, как в Pythony). пока не отдашь GIL) - person DavidW; 03.02.2017
comment
Я немного борюсь за хорошую документацию, в которой прямо говорится, что я боюсь. Если я найду некоторые, я свяжу это. Однако это легко проверить: установите несколько запущенных потоков, которые печатают Hello из потока A/B/C... через равные промежутки времени, а затем создайте другой поток, который вызывает функцию C, которая переходит в спящий режим на минуту. - person DavidW; 03.02.2017
comment
Вот ссылка на первый абзац: docs.python.org/3/faq/ Каждая инструкция байт-кода и, следовательно, весь код реализации C, полученный из каждой инструкции, является атомарным с точки зрения программы Python. - person rohitjv; 31.07.2021