Skip to content

Commit c2627d6

Browse files
authored
gh-116322: Add Py_mod_gil module slot (#116882)
This PR adds the ability to enable the GIL if it was disabled at interpreter startup, and modifies the multi-phase module initialization path to enable the GIL when loading a module, unless that module's spec includes a slot indicating it can run safely without the GIL. PEP 703 called the constant for the slot `Py_mod_gil_not_used`; I went with `Py_MOD_GIL_NOT_USED` for consistency with gh-104148. A warning will be issued up to once per interpreter for the first GIL-using module that is loaded. If `-v` is given, a shorter message will be printed to stderr every time a GIL-using module is loaded (including the first one that issues a warning).
1 parent 3e818af commit c2627d6

File tree

123 files changed

+376
-62
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+376
-62
lines changed

Doc/c-api/module.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,31 @@ The available slot types are:
411411
412412
.. versionadded:: 3.12
413413
414+
.. c:macro:: Py_mod_gil
415+
416+
Specifies one of the following values:
417+
418+
.. c:macro:: Py_MOD_GIL_USED
419+
420+
The module depends on the presence of the global interpreter lock (GIL),
421+
and may access global state without synchronization.
422+
423+
.. c:macro:: Py_MOD_GIL_NOT_USED
424+
425+
The module is safe to run without an active GIL.
426+
427+
This slot is ignored by Python builds not configured with
428+
:option:`--disable-gil`. Otherwise, it determines whether or not importing
429+
this module will cause the GIL to be automatically enabled. See
430+
:envvar:`PYTHON_GIL` and :option:`-X gil <-X>` for more detail.
431+
432+
Multiple ``Py_mod_gil`` slots may not be specified in one module definition.
433+
434+
If ``Py_mod_gil`` is not specified, the import machinery defaults to
435+
``Py_MOD_GIL_USED``.
436+
437+
.. versionadded: 3.13
438+
414439
See :PEP:`489` for more details on multi-phase initialization.
415440
416441
Low-level module creation functions
@@ -609,6 +634,19 @@ state:
609634
610635
.. versionadded:: 3.9
611636
637+
.. c:function:: int PyModule_ExperimentalSetGIL(PyObject *module, void *gil)
638+
639+
Indicate that *module* does or does not support running without the global
640+
interpreter lock (GIL), using one of the values from
641+
:c:macro:`Py_mod_gil`. It must be called during *module*'s initialization
642+
function. If this function is not called during module initialization, the
643+
import machinery assumes the module does not support running without the
644+
GIL. This function is only available in Python builds configured with
645+
:option:`--disable-gil`.
646+
Return ``-1`` on error, ``0`` on success.
647+
648+
.. versionadded:: 3.13
649+
612650
613651
Module lookup
614652
^^^^^^^^^^^^^

Include/internal/pycore_moduleobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ typedef struct {
2222
PyObject *md_weaklist;
2323
// for logging purposes after md_dict is cleared
2424
PyObject *md_name;
25+
#ifdef Py_GIL_DISABLED
26+
void *md_gil;
27+
#endif
2528
} PyModuleObject;
2629

2730
static inline PyModuleDef* _PyModule_GetDef(PyObject *mod) {

Include/moduleobject.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,13 @@ struct PyModuleDef_Slot {
7676
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030c0000
7777
# define Py_mod_multiple_interpreters 3
7878
#endif
79+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
80+
# define Py_mod_gil 4
81+
#endif
82+
7983

8084
#ifndef Py_LIMITED_API
81-
#define _Py_mod_LAST_SLOT 3
85+
#define _Py_mod_LAST_SLOT 4
8286
#endif
8387

8488
#endif /* New in 3.5 */
@@ -90,6 +94,16 @@ struct PyModuleDef_Slot {
9094
# define Py_MOD_PER_INTERPRETER_GIL_SUPPORTED ((void *)2)
9195
#endif
9296

97+
/* for Py_mod_gil: */
98+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
99+
# define Py_MOD_GIL_USED ((void *)0)
100+
# define Py_MOD_GIL_NOT_USED ((void *)1)
101+
#endif
102+
103+
#if !defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED)
104+
PyAPI_FUNC(int) PyModule_ExperimentalSetGIL(PyObject *module, void *gil);
105+
#endif
106+
93107
struct PyModuleDef {
94108
PyModuleDef_Base m_base;
95109
const char* m_name;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import types
2+
import unittest
3+
from test.test_importlib import util
4+
5+
machinery = util.import_importlib('importlib.machinery')
6+
7+
from test.test_importlib.extension.test_loader import MultiPhaseExtensionModuleTests
8+
9+
10+
class NonModuleExtensionTests:
11+
setUp = MultiPhaseExtensionModuleTests.setUp
12+
load_module_by_name = MultiPhaseExtensionModuleTests.load_module_by_name
13+
14+
def _test_nonmodule(self):
15+
# Test returning a non-module object from create works.
16+
name = self.name + '_nonmodule'
17+
mod = self.load_module_by_name(name)
18+
self.assertNotEqual(type(mod), type(unittest))
19+
self.assertEqual(mod.three, 3)
20+
21+
# issue 27782
22+
def test_nonmodule_with_methods(self):
23+
# Test creating a non-module object with methods defined.
24+
name = self.name + '_nonmodule_with_methods'
25+
mod = self.load_module_by_name(name)
26+
self.assertNotEqual(type(mod), type(unittest))
27+
self.assertEqual(mod.three, 3)
28+
self.assertEqual(mod.bar(10, 1), 9)
29+
30+
def test_null_slots(self):
31+
# Test that NULL slots aren't a problem.
32+
name = self.name + '_null_slots'
33+
module = self.load_module_by_name(name)
34+
self.assertIsInstance(module, types.ModuleType)
35+
self.assertEqual(module.__name__, name)
36+
37+
38+
(Frozen_NonModuleExtensionTests,
39+
Source_NonModuleExtensionTests
40+
) = util.test_both(NonModuleExtensionTests, machinery=machinery)
41+
42+
43+
if __name__ == '__main__':
44+
unittest.main()

Lib/test/test_importlib/extension/test_loader.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import warnings
1111
import importlib.util
1212
import importlib
13-
from test.support import MISSING_C_DOCSTRINGS
13+
from test import support
14+
from test.support import MISSING_C_DOCSTRINGS, script_helper
1415

1516

1617
class LoaderTests:
@@ -325,29 +326,6 @@ def test_unloadable_nonascii(self):
325326
self.load_module_by_name(name)
326327
self.assertEqual(cm.exception.name, name)
327328

328-
def test_nonmodule(self):
329-
# Test returning a non-module object from create works.
330-
name = self.name + '_nonmodule'
331-
mod = self.load_module_by_name(name)
332-
self.assertNotEqual(type(mod), type(unittest))
333-
self.assertEqual(mod.three, 3)
334-
335-
# issue 27782
336-
def test_nonmodule_with_methods(self):
337-
# Test creating a non-module object with methods defined.
338-
name = self.name + '_nonmodule_with_methods'
339-
mod = self.load_module_by_name(name)
340-
self.assertNotEqual(type(mod), type(unittest))
341-
self.assertEqual(mod.three, 3)
342-
self.assertEqual(mod.bar(10, 1), 9)
343-
344-
def test_null_slots(self):
345-
# Test that NULL slots aren't a problem.
346-
name = self.name + '_null_slots'
347-
module = self.load_module_by_name(name)
348-
self.assertIsInstance(module, types.ModuleType)
349-
self.assertEqual(module.__name__, name)
350-
351329
def test_bad_modules(self):
352330
# Test SystemError is raised for misbehaving extensions.
353331
for name_base in [
@@ -401,5 +379,14 @@ def test_nonascii(self):
401379
) = util.test_both(MultiPhaseExtensionModuleTests, machinery=machinery)
402380

403381

382+
class NonModuleExtensionTests(unittest.TestCase):
383+
def test_nonmodule_cases(self):
384+
# The test cases in this file cause the GIL to be enabled permanently
385+
# in free-threaded builds, so they are run in a subprocess to isolate
386+
# this effect.
387+
script = support.findfile("test_importlib/extension/_test_nonmodule_cases.py")
388+
script_helper.run_test_script(script)
389+
390+
404391
if __name__ == '__main__':
405392
unittest.main()

Lib/test/test_sys.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1606,7 +1606,10 @@ def get_gen(): yield 1
16061606
check(int(PyLong_BASE**2-1), vsize('') + 2*self.longdigit)
16071607
check(int(PyLong_BASE**2), vsize('') + 3*self.longdigit)
16081608
# module
1609-
check(unittest, size('PnPPP'))
1609+
if support.Py_GIL_DISABLED:
1610+
check(unittest, size('PPPPPP'))
1611+
else:
1612+
check(unittest, size('PPPPP'))
16101613
# None
16111614
check(None, size(''))
16121615
# NotImplementedType
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Extension modules may indicate to the runtime that they can run without the
2+
GIL. Multi-phase init modules do so by calling providing
3+
``Py_MOD_GIL_NOT_USED`` for the ``Py_mod_gil`` slot, while single-phase init
4+
modules call ``PyModule_ExperimentalSetGIL(mod, Py_MOD_GIL_NOT_USED)`` from
5+
their init function.

Modules/_abc.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ _abcmodule_free(void *module)
970970
static PyModuleDef_Slot _abcmodule_slots[] = {
971971
{Py_mod_exec, _abcmodule_exec},
972972
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
973+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
973974
{0, NULL}
974975
};
975976

Modules/_asynciomodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3795,6 +3795,7 @@ module_exec(PyObject *mod)
37953795
static struct PyModuleDef_Slot module_slots[] = {
37963796
{Py_mod_exec, module_exec},
37973797
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
3798+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
37983799
{0, NULL},
37993800
};
38003801

Modules/_bisectmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ bisect_modexec(PyObject *m)
462462
static PyModuleDef_Slot bisect_slots[] = {
463463
{Py_mod_exec, bisect_modexec},
464464
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
465+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
465466
{0, NULL}
466467
};
467468

0 commit comments

Comments
 (0)