Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 #116338

Merged
merged 11 commits into from
Mar 11, 2024
18 changes: 18 additions & 0 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_gil.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Include/internal/pycore_initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/_test_embed_set_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import sys
import unittest
from test import support
from test.support import MS_WINDOWS


Expand Down Expand Up @@ -211,6 +212,19 @@ def test_flags(self):
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"])
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,39 @@ def test_pythondevmode_env(self):
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):
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions Misc/python.man
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -438,6 +448,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;
Expand Down
45 changes: 45 additions & 0 deletions Python/initconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
}


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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}
};

Expand Down Expand Up @@ -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;
Expand Down
Loading