Skip to content

destroy_gil() is not called over multiple Py_Initialize() / Py_Finalize() calls #104324

Open
@adr26

Description

@adr26

The following simple balanced calls to Py_Initialize() / Py_FinalizeEx() trigger an error when running under Microsoft's Application Verifier on Windows:

  Py_Initialize();
  Py_FinalizeEx();
  Py_Initialize();
  Py_FinalizeEx();

The above sequence causes there to be two calls to create_gil() for the main GIL, without an intervening call to destroy_gil(): the mismatched use of PyMUTEX_INIT()/PyMUTEX_FINI() (create_gil() calls PyMUTEX_INIT() and destroy_gil() calls PyMUTEX_FINI()) translates into mismatched calls to Win32 APIs InitializeCriticalSection() and DeleteCriticalSection(), which are then detected by Application Verifier.

[Note, however, that this error is not specific to Windows and the GIL is being incorrectly re-initialised without a call to PyMUTEX_FINI() on all platforms.]

The first Py_Initialize() causes a call to create_gil() on interp->ceval.gil for the main interpreter (_PyRuntime._main_interpreter), but as a result of bpo-9901, finalize_interp_delete() (as called from Py_FinalizeEx()) currently defers calling _PyEval_FiniGIL() (and thereby destroy_gil()) until the next Py_Initialize():

static void
finalize_interp_delete(PyInterpreterState *interp)
{
    /* Cleanup auto-thread-state */
    _PyGILState_Fini(interp);

    /* We can't call _PyEval_FiniGIL() here because destroying the GIL lock can
       fail when it is being awaited by another running daemon thread (see
       bpo-9901). Instead pycore_create_interpreter() destroys the previously
       created GIL, which ensures that Py_Initialize / Py_FinalizeEx can be
       called multiple times. */

https://github.com/python/cpython/blame/41aff464cef83d2655029ddd180a51110e8d7f8e/Python/pylifecycle.c#L1740

Unfortunately, the second call Py_Initialize() doesn't call destroy_gil() before calling create_gil() a second time on the main interpreter's GIL, causing this error.

@ericsnowcurrently's comment on init_interp_create_gil() ('XXX This is broken with a per-interpreter GIL'):

static PyStatus
init_interp_create_gil(PyThreadState *tstate, int own_gil)
{
    PyStatus status;

    /* finalize_interp_delete() comment explains why _PyEval_FiniGIL() is
       only called here. */
    // XXX This is broken with a per-interpreter GIL.
    _PyEval_FiniGIL(tstate->interp);

notes that the call to _PyEval_FiniGIL() in init_interp_create_gil() doesn't work with per-interpreter GIL, but it's worse than this as it's broken with just re-creating the main interpreter!

The reason for all this appears to be that the GIL is re-initialised by _PyRuntime_Initialize(), before _PyEval_FiniGIL() has a chance to get its hands on it. Prior to GH-104210 from @ericsnowcurrently landing yesterday (and on previous Python versions), _PyEval_FiniGIL() would detect that the GIL is reinitialised by a prior call to _gil_initialize() within _PyRuntime_Initialize(). However, now the main interpreter (_PyRuntime._main_interpreter) has a GIL pointer (interp->ceval.gil) which is cleared when _PyRuntimeState_Init() resets the runtime to _PyRuntimeState_INIT:

    if (runtime->_initialized) {
        // Py_Initialize() must be running again.
        // Reset to _PyRuntimeState_INIT.
        memcpy(runtime, &initial, sizeof(*runtime));
    }

I have reproduced this with recent cpython commit bf89d4283a28dd00836f2c312a9255f543f93fc7 and have attached a log of the callstacks (avrf-gil.txt).

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions