1
0
mirror of https://github.com/python/cpython synced 2024-07-01 09:24:24 +00:00

gh-116322: Enable the GIL while loading C extension modules (#118560)

Add the ability to enable/disable the GIL at runtime, and use that in
the C module loading code.

We can't know before running a module init function if it supports
free-threading, so the GIL is temporarily enabled before doing so. If
the module declares support for running without the GIL, the GIL is
later disabled. Otherwise, the GIL is permanently enabled, and will
never be disabled again for the life of the current interpreter.
This commit is contained in:
Brett Simmers 2024-05-06 20:07:23 -07:00 committed by GitHub
parent 60bd111844
commit 853163d3b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 352 additions and 32 deletions

View File

@ -131,9 +131,54 @@ extern int _PyEval_ThreadsInitialized(void);
extern void _PyEval_InitGIL(PyThreadState *tstate, int own_gil);
extern void _PyEval_FiniGIL(PyInterpreterState *interp);
extern void _PyEval_AcquireLock(PyThreadState *tstate);
// Acquire the GIL and return 1. In free-threaded builds, this function may
// return 0 to indicate that the GIL was disabled and therefore not acquired.
extern int _PyEval_AcquireLock(PyThreadState *tstate);
extern void _PyEval_ReleaseLock(PyInterpreterState *, PyThreadState *);
#ifdef Py_GIL_DISABLED
// Returns 0 or 1 if the GIL for the given thread's interpreter is disabled or
// enabled, respectively.
//
// The enabled state of the GIL will not change while one or more threads are
// attached.
static inline int
_PyEval_IsGILEnabled(PyThreadState *tstate)
{
return tstate->interp->ceval.gil->enabled != 0;
}
// Enable or disable the GIL used by the interpreter that owns tstate, which
// must be the current thread. This may affect other interpreters, if the GIL
// is shared. All three functions will be no-ops (and return 0) if the
// interpreter's `enable_gil' config is not _PyConfig_GIL_DEFAULT.
//
// Every call to _PyEval_EnableGILTransient() must be paired with exactly one
// call to either _PyEval_EnableGILPermanent() or
// _PyEval_DisableGIL(). _PyEval_EnableGILPermanent() and _PyEval_DisableGIL()
// must only be called while the GIL is enabled from a call to
// _PyEval_EnableGILTransient().
//
// _PyEval_EnableGILTransient() returns 1 if it enabled the GIL, or 0 if the
// GIL was already enabled, whether transiently or permanently. The caller will
// hold the GIL upon return.
//
// _PyEval_EnableGILPermanent() returns 1 if it permanently enabled the GIL
// (which must already be enabled), or 0 if it was already permanently
// enabled. Once _PyEval_EnableGILPermanent() has been called once, all
// subsequent calls to any of the three functions will be no-ops.
//
// _PyEval_DisableGIL() returns 1 if it disabled the GIL, or 0 if the GIL was
// kept enabled because of another request, whether transient or permanent.
//
// All three functions must be called by an attached thread (this implies that
// if the GIL is enabled, the current thread must hold it).
extern int _PyEval_EnableGILTransient(PyThreadState *tstate);
extern int _PyEval_EnableGILPermanent(PyThreadState *tstate);
extern int _PyEval_DisableGIL(PyThreadState *state);
#endif
extern void _PyEval_DeactivateOpCache(void);

View File

@ -21,8 +21,20 @@ extern "C" {
struct _gil_runtime_state {
#ifdef Py_GIL_DISABLED
/* Whether or not this GIL is being used. Can change from 0 to 1 at runtime
if, for example, a module that requires the GIL is loaded. */
/* If this GIL is disabled, enabled == 0.
If this GIL is enabled transiently (most likely to initialize a module
of unknown safety), enabled indicates the number of active transient
requests.
If this GIL is enabled permanently, enabled == INT_MAX.
It must not be modified directly; use _PyEval_EnableGILTransiently(),
_PyEval_EnableGILPermanently(), and _PyEval_DisableGIL()
It is always read and written atomically, but a thread can assume its
value will be stable as long as that thread is attached or knows that no
other threads are attached (e.g., during a stop-the-world.). */
int enabled;
#endif
/* microseconds (the Python API uses seconds, though) */

View File

@ -206,6 +206,19 @@ extern int _PyImport_CheckSubinterpIncompatibleExtensionAllowed(
// Export for '_testinternalcapi' shared extension
PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
#ifdef Py_GIL_DISABLED
// Assuming that the GIL is enabled from a call to
// _PyEval_EnableGILTransient(), resolve the transient request depending on the
// state of the module argument:
// - If module is NULL or a PyModuleObject with md_gil == Py_MOD_GIL_NOT_USED,
// call _PyEval_DisableGIL().
// - Otherwise, call _PyEval_EnableGILPermanent(). If the GIL was not already
// enabled permanently, issue a warning referencing the module's name.
//
// This function may raise an exception.
extern int _PyImport_CheckGILForModule(PyObject *module, PyObject *module_name);
#endif
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1 @@
In ``--disable-gil`` builds, the GIL will be enabled while loading C extension modules. If the module indicates that it supports running without the GIL, the GIL will be disabled once loading is complete. Otherwise, the GIL will remain enabled for the remainder of the interpreter's lifetime. This behavior does not apply if the GIL has been explicitly enabled or disabled with ``PYTHON_GIL`` or ``-Xgil``.

View File

@ -362,12 +362,18 @@ static PyMethodDef wmi_functions[] = {
{ NULL, NULL, 0, NULL }
};
static PyModuleDef_Slot wmi_slots[] = {
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0, NULL},
};
static PyModuleDef wmi_def = {
PyModuleDef_HEAD_INIT,
"_wmi",
NULL, // doc
0, // m_size
wmi_functions
NULL, // doc
0, // m_size
wmi_functions, // m_methods
wmi_slots, // m_slots
};
extern "C" {

View File

@ -205,6 +205,16 @@ static void recreate_gil(struct _gil_runtime_state *gil)
}
#endif
static void
drop_gil_impl(struct _gil_runtime_state *gil)
{
MUTEX_LOCK(gil->mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
_Py_atomic_store_int_relaxed(&gil->locked, 0);
COND_SIGNAL(gil->cond);
MUTEX_UNLOCK(gil->mutex);
}
static void
drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
{
@ -220,7 +230,7 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
struct _gil_runtime_state *gil = ceval->gil;
#ifdef Py_GIL_DISABLED
if (!gil->enabled) {
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
return;
}
#endif
@ -236,11 +246,7 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
_Py_atomic_store_ptr_relaxed(&gil->last_holder, tstate);
}
MUTEX_LOCK(gil->mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
_Py_atomic_store_int_relaxed(&gil->locked, 0);
COND_SIGNAL(gil->cond);
MUTEX_UNLOCK(gil->mutex);
drop_gil_impl(gil);
#ifdef FORCE_SWITCHING
/* We check tstate first in case we might be releasing the GIL for
@ -275,8 +281,10 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
The function saves errno at entry and restores its value at exit.
tstate must be non-NULL. */
static void
tstate must be non-NULL.
Returns 1 if the GIL was acquired, or 0 if not. */
static int
take_gil(PyThreadState *tstate)
{
int err = errno;
@ -300,8 +308,8 @@ take_gil(PyThreadState *tstate)
PyInterpreterState *interp = tstate->interp;
struct _gil_runtime_state *gil = interp->ceval.gil;
#ifdef Py_GIL_DISABLED
if (!gil->enabled) {
return;
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
return 0;
}
#endif
@ -346,6 +354,17 @@ take_gil(PyThreadState *tstate)
}
}
#ifdef Py_GIL_DISABLED
if (!_Py_atomic_load_int_relaxed(&gil->enabled)) {
// Another thread disabled the GIL between our check above and
// now. Don't take the GIL, signal any other waiting threads, and
// return 0.
COND_SIGNAL(gil->cond);
MUTEX_UNLOCK(gil->mutex);
return 0;
}
#endif
#ifdef FORCE_SWITCHING
/* This mutex must be taken before modifying gil->last_holder:
see drop_gil(). */
@ -387,6 +406,7 @@ take_gil(PyThreadState *tstate)
MUTEX_UNLOCK(gil->mutex);
errno = err;
return 1;
}
void _PyEval_SetSwitchInterval(unsigned long microseconds)
@ -451,7 +471,8 @@ init_own_gil(PyInterpreterState *interp, struct _gil_runtime_state *gil)
{
assert(!gil_created(gil));
#ifdef Py_GIL_DISABLED
gil->enabled = _PyInterpreterState_GetConfig(interp)->enable_gil == _PyConfig_GIL_ENABLE;
const PyConfig *config = _PyInterpreterState_GetConfig(interp);
gil->enabled = config->enable_gil == _PyConfig_GIL_ENABLE ? INT_MAX : 0;
#endif
create_gil(gil);
assert(gil_created(gil));
@ -545,11 +566,11 @@ PyEval_ReleaseLock(void)
drop_gil(tstate->interp, tstate);
}
void
int
_PyEval_AcquireLock(PyThreadState *tstate)
{
_Py_EnsureTstateNotNULL(tstate);
take_gil(tstate);
return take_gil(tstate);
}
void
@ -1011,6 +1032,117 @@ _PyEval_InitState(PyInterpreterState *interp)
_gil_initialize(&interp->_gil);
}
#ifdef Py_GIL_DISABLED
int
_PyEval_EnableGILTransient(PyThreadState *tstate)
{
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->enable_gil != _PyConfig_GIL_DEFAULT) {
return 0;
}
struct _gil_runtime_state *gil = tstate->interp->ceval.gil;
int enabled = _Py_atomic_load_int_relaxed(&gil->enabled);
if (enabled == INT_MAX) {
// The GIL is already enabled permanently.
return 0;
}
if (enabled == INT_MAX - 1) {
Py_FatalError("Too many transient requests to enable the GIL");
}
if (enabled > 0) {
// If enabled is nonzero, we know we hold the GIL. This means that no
// other threads are attached, and nobody else can be concurrently
// mutating it.
_Py_atomic_store_int_relaxed(&gil->enabled, enabled + 1);
return 0;
}
// Enabling the GIL changes what it means to be an "attached" thread. To
// safely make this transition, we:
// 1. Detach the current thread.
// 2. Stop the world to detach (and suspend) all other threads.
// 3. Enable the GIL, if nobody else did between our check above and when
// our stop-the-world begins.
// 4. Start the world.
// 5. Attach the current thread. Other threads may attach and hold the GIL
// before this thread, which is harmless.
_PyThreadState_Detach(tstate);
// This could be an interpreter-local stop-the-world in situations where we
// know that this interpreter's GIL is not shared, and that it won't become
// shared before the stop-the-world begins. For now, we always stop all
// interpreters for simplicity.
_PyEval_StopTheWorldAll(&_PyRuntime);
enabled = _Py_atomic_load_int_relaxed(&gil->enabled);
int this_thread_enabled = enabled == 0;
_Py_atomic_store_int_relaxed(&gil->enabled, enabled + 1);
_PyEval_StartTheWorldAll(&_PyRuntime);
_PyThreadState_Attach(tstate);
return this_thread_enabled;
}
int
_PyEval_EnableGILPermanent(PyThreadState *tstate)
{
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->enable_gil != _PyConfig_GIL_DEFAULT) {
return 0;
}
struct _gil_runtime_state *gil = tstate->interp->ceval.gil;
assert(current_thread_holds_gil(gil, tstate));
int enabled = _Py_atomic_load_int_relaxed(&gil->enabled);
if (enabled == INT_MAX) {
return 0;
}
_Py_atomic_store_int_relaxed(&gil->enabled, INT_MAX);
return 1;
}
int
_PyEval_DisableGIL(PyThreadState *tstate)
{
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->enable_gil != _PyConfig_GIL_DEFAULT) {
return 0;
}
struct _gil_runtime_state *gil = tstate->interp->ceval.gil;
assert(current_thread_holds_gil(gil, tstate));
int enabled = _Py_atomic_load_int_relaxed(&gil->enabled);
if (enabled == INT_MAX) {
return 0;
}
assert(enabled >= 1);
enabled--;
// Disabling the GIL is much simpler than enabling it, since we know we are
// the only attached thread. Other threads may start free-threading as soon
// as this store is complete, if it sets gil->enabled to 0.
_Py_atomic_store_int_relaxed(&gil->enabled, enabled);
if (enabled == 0) {
// We're attached, so we know the GIL will remain disabled until at
// least the next time we detach, which must be after this function
// returns.
//
// Drop the GIL, which will wake up any threads waiting in take_gil()
// and let them resume execution without the GIL.
drop_gil_impl(gil);
return 1;
}
return 0;
}
#endif
/* Do periodic things, like check for signals and async I/0.
* We need to do reasonably frequently, but not too frequently.

View File

@ -1,6 +1,7 @@
/* Module definition and import implementation */
#include "Python.h"
#include "pycore_ceval.h"
#include "pycore_hashtable.h" // _Py_hashtable_new_full()
#include "pycore_import.h" // _PyImport_BootstrapImp()
#include "pycore_initconfig.h" // _PyStatus_OK()
@ -1023,6 +1024,12 @@ struct extensions_cache_value {
struct cached_m_dict _m_dict;
_Py_ext_module_origin origin;
#ifdef Py_GIL_DISABLED
/* The module's md_gil slot, for legacy modules that are reinitialized from
m_dict rather than calling their initialization function again. */
void *md_gil;
#endif
};
static struct extensions_cache_value *
@ -1351,7 +1358,7 @@ static struct extensions_cache_value *
_extensions_cache_set(PyObject *path, PyObject *name,
PyModuleDef *def, PyModInitFunction m_init,
Py_ssize_t m_index, PyObject *m_dict,
_Py_ext_module_origin origin)
_Py_ext_module_origin origin, void *md_gil)
{
struct extensions_cache_value *value = NULL;
void *key = NULL;
@ -1401,7 +1408,13 @@ _extensions_cache_set(PyObject *path, PyObject *name,
.m_index=m_index,
/* m_dict is set by set_cached_m_dict(). */
.origin=origin,
#ifdef Py_GIL_DISABLED
.md_gil=md_gil,
#endif
};
#ifndef Py_GIL_DISABLED
(void)md_gil;
#endif
if (init_cached_m_dict(newvalue, m_dict) < 0) {
goto finally;
}
@ -1526,6 +1539,47 @@ _PyImport_CheckSubinterpIncompatibleExtensionAllowed(const char *name)
return 0;
}
#ifdef Py_GIL_DISABLED
int
_PyImport_CheckGILForModule(PyObject* module, PyObject *module_name)
{
PyThreadState *tstate = _PyThreadState_GET();
if (module == NULL) {
_PyEval_DisableGIL(tstate);
return 0;
}
if (!PyModule_Check(module) ||
((PyModuleObject *)module)->md_gil == Py_MOD_GIL_USED) {
if (_PyEval_EnableGILPermanent(tstate)) {
int warn_result = PyErr_WarnFormat(
PyExc_RuntimeWarning,
1,
"The global interpreter lock (GIL) has been enabled to load "
"module '%U', which has not declared that it can run safely "
"without the GIL. To override this behavior and keep the GIL "
"disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.",
module_name
);
if (warn_result < 0) {
return warn_result;
}
}
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->enable_gil == _PyConfig_GIL_DEFAULT && config->verbose) {
PySys_FormatStderr("# loading module '%U', which requires the GIL\n",
module_name);
}
}
else {
_PyEval_DisableGIL(tstate);
}
return 0;
}
#endif
static PyObject *
get_core_module_dict(PyInterpreterState *interp,
PyObject *name, PyObject *path)
@ -1625,6 +1679,7 @@ struct singlephase_global_update {
Py_ssize_t m_index;
PyObject *m_dict;
_Py_ext_module_origin origin;
void *md_gil;
};
static struct extensions_cache_value *
@ -1683,7 +1738,7 @@ update_global_state_for_extension(PyThreadState *tstate,
#endif
cached = _extensions_cache_set(
path, name, def, m_init, singlephase->m_index, m_dict,
singlephase->origin);
singlephase->origin, singlephase->md_gil);
if (cached == NULL) {
// XXX Ignore this error? Doing so would effectively
// mark the module as not loadable.
@ -1768,6 +1823,13 @@ reload_singlephase_extension(PyThreadState *tstate,
Py_DECREF(mod);
return NULL;
}
#ifdef Py_GIL_DISABLED
if (def->m_base.m_copy != NULL) {
// For non-core modules, fetch the GIL slot that was stored by
// import_run_extension().
((PyModuleObject *)mod)->md_gil = cached->md_gil;
}
#endif
/* We can't set mod->md_def if it's missing,
* because _PyImport_ClearModulesByIndex() might break
* due to violating interpreter isolation.
@ -1921,6 +1983,9 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
// cache is less reliable than it should be).
.m_index=def->m_base.m_index,
.origin=info->origin,
#ifdef Py_GIL_DISABLED
.md_gil=((PyModuleObject *)mod)->md_gil,
#endif
};
// gh-88216: Extensions and def->m_base.m_copy can be updated
// when the extension module doesn't support sub-interpreters.
@ -2039,6 +2104,10 @@ _PyImport_FixupBuiltin(PyThreadState *tstate, PyObject *mod, const char *name,
/* We don't want def->m_base.m_copy populated. */
.m_dict=NULL,
.origin=_Py_ext_module_origin_CORE,
#ifdef Py_GIL_DISABLED
/* Unused when m_dict == NULL. */
.md_gil=NULL,
#endif
};
cached = update_global_state_for_extension(
tstate, nameobj, nameobj, def, &singlephase);
@ -2128,9 +2197,23 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
goto finally;
}
#ifdef Py_GIL_DISABLED
// This call (and the corresponding call to _PyImport_CheckGILForModule())
// would ideally be inside import_run_extension(). They are kept in the
// callers for now because that would complicate the control flow inside
// import_run_extension(). It should be possible to restructure
// import_run_extension() to address this.
_PyEval_EnableGILTransient(tstate);
#endif
/* Now load it. */
mod = import_run_extension(
tstate, p0, &info, spec, get_modules_dict(tstate, true));
#ifdef Py_GIL_DISABLED
if (_PyImport_CheckGILForModule(mod, info.name) < 0) {
Py_CLEAR(mod);
goto finally;
}
#endif
finally:
_Py_ext_module_loader_info_clear(&info);
@ -4505,10 +4588,22 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
goto finally;
}
#ifdef Py_GIL_DISABLED
// This call (and the corresponding call to _PyImport_CheckGILForModule())
// would ideally be inside import_run_extension(). They are kept in the
// callers for now because that would complicate the control flow inside
// import_run_extension(). It should be possible to restructure
// import_run_extension() to address this.
_PyEval_EnableGILTransient(tstate);
#endif
mod = import_run_extension(
tstate, p0, &info, spec, get_modules_dict(tstate, true));
if (mod == NULL) {
#ifdef Py_GIL_DISABLED
if (_PyImport_CheckGILForModule(mod, info.name) < 0) {
Py_CLEAR(mod);
goto finally;
}
#endif
// XXX Shouldn't this happen in the error cases too (i.e. in "finally")?
if (fp) {

View File

@ -2057,19 +2057,36 @@ _PyThreadState_Attach(PyThreadState *tstate)
Py_FatalError("non-NULL old thread state");
}
_PyEval_AcquireLock(tstate);
// XXX assert(tstate_is_alive(tstate));
current_fast_set(&_PyRuntime, tstate);
tstate_activate(tstate);
while (1) {
int acquired_gil = _PyEval_AcquireLock(tstate);
if (!tstate_try_attach(tstate)) {
tstate_wait_attach(tstate);
}
// XXX assert(tstate_is_alive(tstate));
current_fast_set(&_PyRuntime, tstate);
tstate_activate(tstate);
if (!tstate_try_attach(tstate)) {
tstate_wait_attach(tstate);
}
#ifdef Py_GIL_DISABLED
_Py_qsbr_attach(((_PyThreadStateImpl *)tstate)->qsbr);
if (_PyEval_IsGILEnabled(tstate) != acquired_gil) {
// The GIL was enabled between our call to _PyEval_AcquireLock()
// and when we attached (the GIL can't go from enabled to disabled
// here because only a thread holding the GIL can disable
// it). Detach and try again.
assert(!acquired_gil);
tstate_set_detached(tstate, _Py_THREAD_DETACHED);
tstate_deactivate(tstate);
current_fast_clear(&_PyRuntime);
continue;
}
_Py_qsbr_attach(((_PyThreadStateImpl *)tstate)->qsbr);
#else
(void)acquired_gil;
#endif
break;
}
// Resume previous critical section. This acquires the lock(s) from the
// top-most critical section.

View File

@ -2433,8 +2433,7 @@ sys__is_gil_enabled_impl(PyObject *module)
/*[clinic end generated code: output=57732cf53f5b9120 input=7e9c47f15a00e809]*/
{
#ifdef Py_GIL_DISABLED
PyInterpreterState *interp = _PyInterpreterState_GET();
return interp->ceval.gil->enabled;
return _PyEval_IsGILEnabled(_PyThreadState_GET());
#else
return 1;
#endif