Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Include/internal/pycore_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,18 @@ PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
// 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.
// - Otherwise, call _PyImport_EnableGILAndWarn
//
// This function may raise an exception.
extern int _PyImport_CheckGILForModule(PyObject *module, PyObject *module_name);
// Assuming that the GIL is enabled from a call to
// _PyEval_EnableGILTransient(), call _PyEval_EnableGILPermanent().
// If the GIL was not already enabled permanently, issue a warning referencing
// the module's name.
// Leave a message in verbose mode.
//
// This function may raise an exception.
extern int _PyImport_EnableGILAndWarn(PyThreadState *, PyObject *module_name);
#endif

#ifdef __cplusplus
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import builtins
import concurrent.interpreters
import errno
import glob
import json
Expand Down Expand Up @@ -3407,6 +3408,35 @@ def test_from_modexport(self):

self.assertEqual(module.__name__, modname)

def test_from_modexport_gil_used(self):
# Test that a module with Py_MOD_GIL_USED (re-)enables the GIL.
# Do this in a new interpreter to avoid interfering with global state.
modname = '_test_from_modexport_gil_used'
filename = _testmultiphase.__file__
interp = concurrent.interpreters.create()
self.addCleanup(interp.close)
queue = concurrent.interpreters.create_queue()
interp.prepare_main(
modname=modname,
filename=filename,
queue=queue,
)
enabled_before = sys._is_gil_enabled()
interp.exec(f"""if True:
import sys
from {__name__} import import_extension_from_file
module = import_extension_from_file(modname, filename,
put_in_sys_modules=False)
queue.put(module.__name__)
queue.put(sys._is_gil_enabled())
""")

self.assertEqual(queue.get(), modname)
self.assertEqual(queue.get(), True)
self.assertTrue(queue.empty())

self.assertEqual(enabled_before, sys._is_gil_enabled())

def test_from_modexport_null(self):
modname = '_test_from_modexport_null'
filename = _testmultiphase.__file__
Expand All @@ -3428,6 +3458,35 @@ def test_from_modexport_create_nonmodule(self):
put_in_sys_modules=False)
self.assertIsInstance(module, str)

def test_from_modexport_create_nonmodule_gil_used(self):
# Test that a module with Py_MOD_GIL_USED (re-)enables the GIL.
# Do this in a new interpreter to avoid interfering with global state.
modname = '_test_from_modexport_create_nonmodule_gil_used'
filename = _testmultiphase.__file__
interp = concurrent.interpreters.create()
self.addCleanup(interp.close)
queue = concurrent.interpreters.create_queue()
interp.prepare_main(
modname=modname,
filename=filename,
queue=queue,
)
enabled_before = sys._is_gil_enabled()
interp.exec(f"""if True:
import sys
from {__name__} import import_extension_from_file
module = import_extension_from_file(modname, filename,
put_in_sys_modules=False)
queue.put(module)
queue.put(sys._is_gil_enabled())
""")

self.assertIsInstance(queue.get(), str)
self.assertEqual(queue.get(), True)
self.assertTrue(queue.empty())

self.assertEqual(enabled_before, sys._is_gil_enabled())

def test_from_modexport_smoke(self):
# General positive test for sundry features
# (PyModule_FromSlotsAndSpec tests exercise these more carefully)
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_importlib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import tempfile
import types

_testsinglephase = import_helper.import_module("_testsinglephase")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_testsinglephase is unusable in subinterpreters.

# gh-116303: Skip test module dependent tests if test modules are unavailable
import_helper.import_module("_testmultiphase")


BUILTINS = types.SimpleNamespace()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :c:macro:`Py_mod_gil` with API added in :pep:`793`:
:c:func:`!PyModule_FromSlotsAndSpec` and ``PyModExport`` hooks
31 changes: 31 additions & 0 deletions Modules/_testmultiphase.c
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,20 @@ PyModExport__test_from_modexport(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_name, "_test_from_modexport"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
};
return slots;
}

PyMODEXPORT_FUNC
PyModExport__test_from_modexport_gil_used(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_name, "_test_from_modexport_gil_used"},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_USED},
{0},
};
return slots;
Expand Down Expand Up @@ -1073,6 +1087,21 @@ PyModExport__test_from_modexport_create_nonmodule(void)
static PyModuleDef_Slot slots[] = {
{Py_mod_name, "_test_from_modexport_create_nonmodule"},
{Py_mod_create, modexport_create_string},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
};
return slots;
}

PyMODEXPORT_FUNC
PyModExport__test_from_modexport_create_nonmodule_gil_used(void)
{
static PyModuleDef_Slot slots[] = {
{Py_mod_name, "_test_from_modexport_create_nonmodule"},
{Py_mod_create, modexport_create_string},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_USED},
{0},
};
return slots;
Expand Down Expand Up @@ -1165,6 +1194,8 @@ PyModExport__test_from_modexport_smoke(void)
{Py_mod_methods, methods},
{Py_mod_state_free, modexport_smoke_free},
{Py_mod_token, (void*)&modexport_smoke_test_token},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0},
};
return slots;
Expand Down
17 changes: 17 additions & 0 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "Python.h"
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_ceval.h" // _PyEval_EnableGILTransient()
#include "pycore_dict.h" // _PyDict_EnablePerThreadRefcounting()
#include "pycore_fileutils.h" // _Py_wgetcwd
#include "pycore_import.h" // _PyImport_GetNextModuleIndex()
Expand Down Expand Up @@ -522,6 +523,22 @@ module_from_def_and_spec(
#undef COPY_COMMON_SLOT
}

#ifdef Py_GIL_DISABLED
// For modules created directly from slots (not from a def), we enable
// the GIL here (pairing `_PyEval_EnableGILTransient` with
// an immediate `_PyImport_EnableGILAndWarn`).
// For modules created from a def, the caller is responsible for this.
if (!original_def && requires_gil) {
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_EnableGILTransient(tstate) < 0) {
goto error;
}
if (_PyImport_EnableGILAndWarn(tstate, nameobj) < 0) {
goto error;
}
}
#endif

/* By default, multi-phase init modules are expected
to work under multiple interpreters. */
if (!has_multiple_interpreters_slot) {
Expand Down
45 changes: 26 additions & 19 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -1550,25 +1550,9 @@ _PyImport_CheckGILForModule(PyObject* module, PyObject *module_name)
if (!PyModule_Check(module) ||
((PyModuleObject *)module)->md_requires_gil)
{
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);
assert(((PyModuleObject *)module)->md_token_is_def);
if (_PyImport_EnableGILAndWarn(tstate, module_name) < 0) {
return -1;
}
}
else {
Expand All @@ -1577,6 +1561,27 @@ _PyImport_CheckGILForModule(PyObject* module, PyObject *module_name)

return 0;
}

int
_PyImport_EnableGILAndWarn(PyThreadState *tstate, PyObject *module_name) {
if (_PyEval_EnableGILPermanent(tstate)) {
return 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
);
}
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);
}
return 0;
}
#endif

static PyThreadState *
Expand Down Expand Up @@ -4785,6 +4790,8 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
_PyImport_GetModuleExportHooks(&info, fp, &p0, &ex0);
if (ex0) {
mod = import_run_modexport(tstate, ex0, &info, spec);
// Modules created from slots handle GIL enablement (Py_mod_gil slot)
// when they're created.
goto cleanup;
}
if (p0 == NULL) {
Expand Down
Loading