]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-116167: Allow disabling the GIL with `PYTHON_GIL=0` or `-X gil=0` (#116338)
authorBrett Simmers <swtaarrs@users.noreply.github.com>
Mon, 11 Mar 2024 15:02:58 +0000 (11:02 -0400)
committerGitHub <noreply@github.com>
Mon, 11 Mar 2024 15:02:58 +0000 (11:02 -0400)
In free-threaded builds, running with `PYTHON_GIL=0` will now disable the
GIL. Follow-up issues track work to re-enable the GIL when loading an
incompatible extension, and to disable the GIL by default.

In order to support re-enabling the GIL at runtime, all GIL-related data
structures are initialized as usual, and disabling the GIL simply sets a flag
that causes `take_gil()` and `drop_gil()` to return early.

12 files changed:
Doc/using/cmdline.rst
Include/cpython/initconfig.h
Include/internal/pycore_gil.h
Include/internal/pycore_initconfig.h
Lib/subprocess.py
Lib/test/_test_embed_set_config.py
Lib/test/test_cmd_line.py
Lib/test/test_embed.py
Misc/python.man
Python/ceval_gil.c
Python/initconfig.c
Python/sysmodule.c

index 0a7f6363a2b628d4bc313ee536ed2f7ebde5a681..36cddffb9eae34f37f895e718ef4fe5352221f76 100644 (file)
@@ -559,6 +559,9 @@ Miscellaneous options
      :mod:`__main__`. This can be used to execute code early during Python
      initialization. Python needs to be :ref:`built in debug mode <debug-build>`
      for this option to exist.  See also :envvar:`PYTHON_PRESITE`.
+   * :samp:`-X gil={0,1}` forces the GIL to be disabled or enabled,
+     respectively. Only available in builds configured with
+     :option:`--disable-gil`. See also :envvar:`PYTHON_GIL`.
 
    It also allows passing arbitrary values and retrieving them through the
    :data:`sys._xoptions` dictionary.
@@ -601,6 +604,9 @@ Miscellaneous options
    .. versionchanged:: 3.13
       Added the ``-X cpu_count`` and ``-X presite`` options.
 
+   .. versionchanged:: 3.13
+      Added the ``-X gil`` option.
+
 .. _using-on-controlling-color:
 
 Controlling color
@@ -1138,6 +1144,18 @@ conflict.
 
    .. versionadded:: 3.13
 
+.. envvar:: PYTHON_GIL
+
+   If this variable is set to ``1``, the global interpreter lock (GIL) will be
+   forced on. Setting it to ``0`` forces the GIL off.
+
+   See also the :option:`-X gil <-X>` command-line option, which takes
+   precedence over this variable.
+
+   Needs Python configured with the :option:`--disable-gil` build option.
+
+   .. versionadded:: 3.13
+
 Debug-mode variables
 ~~~~~~~~~~~~~~~~~~~~
 
index 87c059c521cbc9271dae6201657419c16d5a9237..5da5ef9e5431b1f79bb5134a35f79ce95d90fea6 100644 (file)
@@ -181,6 +181,9 @@ typedef struct PyConfig {
     int int_max_str_digits;
 
     int cpu_count;
+#ifdef Py_GIL_DISABLED
+    int enable_gil;
+#endif
 
     /* --- Path configuration inputs ------------ */
     int pathconfig_warnings;
index 19b0d23a68568a29f0ca4ef23e203d7e7a4a9a3c..d36b4c0db010b2ed57e8eba36130be2dc03ab0a6 100644 (file)
@@ -20,6 +20,11 @@ extern "C" {
 #define FORCE_SWITCHING
 
 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. */
+    int enabled;
+#endif
     /* microseconds (the Python API uses seconds, though) */
     unsigned long interval;
     /* Last PyThreadState holding / having held the GIL. This helps us
index c86988234f6a050f0adec70666766a6b8e772561..1c68161341860aa4588e7fdf576737fa01cce99b 100644 (file)
@@ -153,6 +153,18 @@ typedef enum {
     _PyConfig_INIT_ISOLATED = 3
 } _PyConfigInitEnum;
 
+typedef enum {
+    /* For now, this means the GIL is enabled.
+
+       gh-116329: This will eventually change to "the GIL is disabled but can
+       be reenabled by loading an incompatible extension module." */
+    _PyConfig_GIL_DEFAULT = -1,
+
+    /* The GIL has been forced off or on, and will not be affected by module loading. */
+    _PyConfig_GIL_DISABLE = 0,
+    _PyConfig_GIL_ENABLE = 1,
+} _PyConfigGILEnum;
+
 // Export for '_testembed' program
 PyAPI_FUNC(void) _PyConfig_InitCompatConfig(PyConfig *config);
 
index 20db7747d5db13c323c0136b49735589a4a5d4f1..1437bf8148282c0b694c22c099e1d820b5e00d57 100644 (file)
@@ -350,7 +350,7 @@ def _args_from_interpreter_flags():
     if dev_mode:
         args.extend(('-X', 'dev'))
     for opt in ('faulthandler', 'tracemalloc', 'importtime',
-                'frozen_modules', 'showrefcount', 'utf8'):
+                'frozen_modules', 'showrefcount', 'utf8', 'gil'):
         if opt in xoptions:
             value = xoptions[opt]
             if value is True:
index 75b6b7d1b39fa49aa628db6654f58285d99c7b2a..5ff521892cb6fe2affcdbac93d2977df7f20674a 100644 (file)
@@ -9,6 +9,7 @@ import _testinternalcapi
 import os
 import sys
 import unittest
+from test import support
 from test.support import MS_WINDOWS
 
 
@@ -211,6 +212,19 @@ class SetConfigTests(unittest.TestCase):
         self.set_config(use_hash_seed=1, hash_seed=123)
         self.assertEqual(sys.flags.hash_randomization, 1)
 
+        if support.Py_GIL_DISABLED:
+            self.set_config(enable_gil=-1)
+            self.assertEqual(sys.flags.gil, None)
+            self.set_config(enable_gil=0)
+            self.assertEqual(sys.flags.gil, 0)
+            self.set_config(enable_gil=1)
+            self.assertEqual(sys.flags.gil, 1)
+        else:
+            # Builds without Py_GIL_DISABLED don't have
+            # PyConfig.enable_gil. sys.flags.gil is always defined to 1, for
+            # consistency.
+            self.assertEqual(sys.flags.gil, 1)
+
     def test_options(self):
         self.check(warnoptions=[])
         self.check(warnoptions=["default", "ignore"])
index 6796dc6e3570e976112f7f7a078e7e97f099bf52..c633f6493cfab7f6fc3450a5fca88b998bf38be2 100644 (file)
@@ -869,6 +869,39 @@ class CmdLineTest(unittest.TestCase):
         self.assertEqual(proc.stdout.rstrip(), 'True')
         self.assertEqual(proc.returncode, 0, proc)
 
+    @unittest.skipUnless(support.Py_GIL_DISABLED,
+                         "PYTHON_GIL and -X gil only supported in Py_GIL_DISABLED builds")
+    def test_python_gil(self):
+        cases = [
+            # (env, opt, expected, msg)
+            (None, None, 'None', "no options set"),
+            ('0', None, '0', "PYTHON_GIL=0"),
+            ('1', None, '1', "PYTHON_GIL=1"),
+            ('1', '0', '0', "-X gil=0 overrides PYTHON_GIL=1"),
+            (None, '0', '0', "-X gil=0"),
+            (None, '1', '1', "-X gil=1"),
+        ]
+
+        code = "import sys; print(sys.flags.gil)"
+        environ = dict(os.environ)
+
+        for env, opt, expected, msg in cases:
+            with self.subTest(msg, env=env, opt=opt):
+                environ.pop('PYTHON_GIL', None)
+                if env is not None:
+                    environ['PYTHON_GIL'] = env
+                extra_args = []
+                if opt is not None:
+                    extra_args = ['-X', f'gil={opt}']
+
+                proc = subprocess.run([sys.executable, *extra_args, '-c', code],
+                                      stdout=subprocess.PIPE,
+                                      stderr=subprocess.PIPE,
+                                      text=True, env=environ)
+                self.assertEqual(proc.returncode, 0, proc)
+                self.assertEqual(proc.stdout.rstrip(), expected)
+                self.assertEqual(proc.stderr, '')
+
     @unittest.skipUnless(sys.platform == 'win32',
                          'bpo-32457 only applies on Windows')
     def test_argv0_normalization(self):
index 55d3acf448540afeac0afd8aa2a3738f06ef8ee0..ab1d579ed12755e9ea4105db7f272211e8b10763 100644 (file)
@@ -523,6 +523,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         CONFIG_COMPAT['_pystats'] = 0
     if support.Py_DEBUG:
         CONFIG_COMPAT['run_presite'] = None
+    if support.Py_GIL_DISABLED:
+        CONFIG_COMPAT['enable_gil'] = -1
     if MS_WINDOWS:
         CONFIG_COMPAT.update({
             'legacy_windows_stdio': 0,
index 0f5dfa2e2289f71850a8bbf670f8589e32355866..4c90c0e2a998ba21c9a694822118376b76292958 100644 (file)
@@ -607,6 +607,10 @@ output. Setting it to 0 deactivates this behavior.
 .IP PYTHON_HISTORY
 This environment variable can be used to set the location of a history file
 (on Unix, it is \fI~/.python_history\fP by default).
+.IP PYTHON_GIL
+If this variable is set to 1, the global interpreter lock (GIL) will be forced
+on. Setting it to 0 forces the GIL off. Only available in builds configured
+with \fB--disable-gil\fP.
 .SS Debug-mode variables
 Setting these variables only has an effect in a debug build of Python, that is,
 if Python was configured with the
index edfc466d9f20ec02a8f2ead46b4165e0a262caac..d2cd35dfa86833ee7b47563aedbdc966399ae426 100644 (file)
@@ -219,6 +219,11 @@ drop_gil(PyInterpreterState *interp, PyThreadState *tstate)
     // XXX assert(tstate == NULL || !tstate->_status.cleared);
 
     struct _gil_runtime_state *gil = ceval->gil;
+#ifdef Py_GIL_DISABLED
+    if (!gil->enabled) {
+        return;
+    }
+#endif
     if (!_Py_atomic_load_ptr_relaxed(&gil->locked)) {
         Py_FatalError("drop_gil: GIL is not locked");
     }
@@ -294,6 +299,11 @@ take_gil(PyThreadState *tstate)
     assert(_PyThreadState_CheckConsistency(tstate));
     PyInterpreterState *interp = tstate->interp;
     struct _gil_runtime_state *gil = interp->ceval.gil;
+#ifdef Py_GIL_DISABLED
+    if (!gil->enabled) {
+        return;
+    }
+#endif
 
     /* Check that _PyEval_InitThreads() was called to create the lock */
     assert(gil_created(gil));
@@ -440,6 +450,11 @@ static void
 init_own_gil(PyInterpreterState *interp, struct _gil_runtime_state *gil)
 {
     assert(!gil_created(gil));
+#ifdef Py_GIL_DISABLED
+    // gh-116329: Once it is safe to do so, change this condition to
+    // (enable_gil == _PyConfig_GIL_ENABLE), so the GIL is disabled by default.
+    gil->enabled = _PyInterpreterState_GetConfig(interp)->enable_gil != _PyConfig_GIL_DISABLE;
+#endif
     create_gil(gil);
     assert(gil_created(gil));
     interp->ceval.gil = gil;
index 17c95171d9528a5f9899db2f3d991ef1c152c81c..e3a62e53334163b81ee3ba884f9e663d42043957 100644 (file)
@@ -95,6 +95,9 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
     SPEC(safe_path, BOOL),
     SPEC(int_max_str_digits, INT),
     SPEC(cpu_count, INT),
+#ifdef Py_GIL_DISABLED
+    SPEC(enable_gil, INT),
+#endif
     SPEC(pathconfig_warnings, BOOL),
     SPEC(program_name, WSTR),
     SPEC(pythonpath_env, WSTR_OPT),
@@ -278,6 +281,9 @@ static const char usage_envvars[] =
 "PYTHON_CPU_COUNT: Overrides the return value of os.process_cpu_count(),\n"
 "                  os.cpu_count(), and multiprocessing.cpu_count() if set to\n"
 "                  a positive integer.\n"
+#ifdef Py_GIL_DISABLED
+"PYTHON_GIL      : When set to 0, disables the GIL.\n"
+#endif
 "PYTHONDEVMODE   : enable the development mode.\n"
 "PYTHONPYCACHEPREFIX: root directory for bytecode cache (pyc) files.\n"
 "PYTHONWARNDEFAULTENCODING: enable opt-in EncodingWarning for 'encoding=None'.\n"
@@ -862,6 +868,9 @@ _PyConfig_InitCompatConfig(PyConfig *config)
     config->_is_python_build = 0;
     config->code_debug_ranges = 1;
     config->cpu_count = -1;
+#ifdef Py_GIL_DISABLED
+    config->enable_gil = _PyConfig_GIL_DEFAULT;
+#endif
 }
 
 
@@ -1574,6 +1583,24 @@ config_wstr_to_int(const wchar_t *wstr, int *result)
     return 0;
 }
 
+static PyStatus
+config_read_gil(PyConfig *config, size_t len, wchar_t first_char)
+{
+#ifdef Py_GIL_DISABLED
+    if (len == 1 && first_char == L'0') {
+        config->enable_gil = _PyConfig_GIL_DISABLE;
+    }
+    else if (len == 1 && first_char == L'1') {
+        config->enable_gil = _PyConfig_GIL_ENABLE;
+    }
+    else {
+        return _PyStatus_ERR("PYTHON_GIL / -X gil must be \"0\" or \"1\"");
+    }
+    return _PyStatus_OK();
+#else
+    return _PyStatus_ERR("PYTHON_GIL / -X gil are not supported by this build");
+#endif
+}
 
 static PyStatus
 config_read_env_vars(PyConfig *config)
@@ -1652,6 +1679,15 @@ config_read_env_vars(PyConfig *config)
         config->safe_path = 1;
     }
 
+    const char *gil = config_get_env(config, "PYTHON_GIL");
+    if (gil != NULL) {
+        size_t len = strlen(gil);
+        status = config_read_gil(config, len, gil[0]);
+        if (_PyStatus_EXCEPTION(status)) {
+            return status;
+        }
+    }
+
     return _PyStatus_OK();
 }
 
@@ -2207,6 +2243,15 @@ config_read(PyConfig *config, int compute_path_config)
         config->show_ref_count = 1;
     }
 
+    const wchar_t *x_gil = config_get_xoption_value(config, L"gil");
+    if (x_gil != NULL) {
+        size_t len = wcslen(x_gil);
+        status = config_read_gil(config, len, x_gil[0]);
+        if (_PyStatus_EXCEPTION(status)) {
+            return status;
+        }
+    }
+
 #ifdef Py_STATS
     if (config_get_xoption(config, L"pystats")) {
         config->_pystats = 1;
index a4161da02980a738340f2c328f0fdaf1fdf1b3f9..cd193c1581c679451b7af98c0f44e91145f03e6f 100644 (file)
@@ -3048,6 +3048,7 @@ static PyStructSequence_Field flags_fields[] = {
     {"warn_default_encoding",   "-X warn_default_encoding"},
     {"safe_path", "-P"},
     {"int_max_str_digits",      "-X int_max_str_digits"},
+    {"gil",                     "-X gil"},
     {0}
 };
 
@@ -3097,6 +3098,16 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
     SetFlag(config->warn_default_encoding);
     SetFlagObj(PyBool_FromLong(config->safe_path));
     SetFlag(config->int_max_str_digits);
+#ifdef Py_GIL_DISABLED
+    if (config->enable_gil == _PyConfig_GIL_DEFAULT) {
+        SetFlagObj(Py_NewRef(Py_None));
+    }
+    else {
+        SetFlag(config->enable_gil);
+    }
+#else
+    SetFlagObj(PyLong_FromLong(1));
+#endif
 #undef SetFlagObj
 #undef SetFlag
     return 0;