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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy