The rule is easy: you need to hold the GIL to access Python machinery (any API starting with Py<...> and any PyObject).

So, you can release it whenever you don't need any of that.

Anything further than this is the fundamental problem of locking granularity: potential benefits vs locking overhead. There was an experiment for Py 1.4 to replace the GIL with more granular locks that failed exactly because the overhead proved prohibitive.

That's why it's typically released for code chunks involving call(s) to extental facilities that can take arbitrary time (especially if they involve waiting for external events) -- if you don't release the lock, Python will be just idling during this time.


Heeding this rule, you will get to your goal automatically: whenever a thread can't proceed further (whether it's I/O, signal from another thread, or even so much as a time.sleep() to avoid a busy loop), it will release the lock and allow other threads to proceed in its stead. The GIL assigning mechanism strives to be fair (see issue8299 for exploration on how fair it is), releasing the programmer from bothering about any bias stemming solely from the engine.

Answer from ivan_pozdeev on Stack Overflow
🌐
GitHub
github.com › python › cpython › issues › 105182
C API: Remove PyEval_AcquireLock() and PyEval_InitThreads() functions · Issue #105182 · python/cpython
June 1, 2023 - The commit ebeb903 adds this entry to What's New in Python 3.2: * The misleading functions :c:func:`PyEval_AcquireLock()` and :c:func:`PyEval_ReleaseLock()` have been officially deprecated.
Author   vstinner
🌐
GitHub
github.com › python › cpython › issues › 80656
PyEval_AcquireLock() and PyEval_AcquireThread() do not handle runtime finalization properly. · Issue #80656 · python/cpython
March 29, 2019 - 3.8 (EOL)end of lifeend of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error ... Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state. ... assignee = None closed_at = <Date 2019-04-29.09:23:39.689> created_at = <Date 2019-03-29.19:26:20.924> labels = ['interpreter-core', 'type-bug', '3.8'] title = 'PyEval_AcquireLock() and PyEval_AcquireThread() do not handle runtime finalization properly.' updated_at = <Date 2020-03-09.10:53:27.812> user = 'https://github.com/ericsnowcurrently'
Author   ericsnowcurrently
🌐
Weshallneversurrender
weshallneversurrender.com › PyEval-AcquireLock
Understanding PyEval_AcquireLock: The Bouncer at Python's Nightclub | WE SHALL NEVER SURRENDER
June 5, 2024 - `PyEval_AcquireLock` is essentially a function that plays a crucial role in managing the GIL. Think of it as the 'handshake deal' between the bouncer and a thread—a way to say, “Hey, I need some exclusive party time for a while.” When a thread calls `PyEval_AcquireLock`, it’s requesting ...
🌐
Vstinner
vstinner.github.io › gil-bugfixes-daemon-threads-python39.html
GIL bugfixes for daemon threads in Python 3.9 — Victor Stinner blog 3
April 4, 2020 - Eric Snow analyzed the bug and understood that it is related to daemon threads and Python finalization. He identified that PyEval_AcquireLock() and PyEval_AcquireThread() function take the GIL but don't exit the thread if Python is finalizing.
Top answer
1 of 2
1

The rule is easy: you need to hold the GIL to access Python machinery (any API starting with Py<...> and any PyObject).

So, you can release it whenever you don't need any of that.

Anything further than this is the fundamental problem of locking granularity: potential benefits vs locking overhead. There was an experiment for Py 1.4 to replace the GIL with more granular locks that failed exactly because the overhead proved prohibitive.

That's why it's typically released for code chunks involving call(s) to extental facilities that can take arbitrary time (especially if they involve waiting for external events) -- if you don't release the lock, Python will be just idling during this time.


Heeding this rule, you will get to your goal automatically: whenever a thread can't proceed further (whether it's I/O, signal from another thread, or even so much as a time.sleep() to avoid a busy loop), it will release the lock and allow other threads to proceed in its stead. The GIL assigning mechanism strives to be fair (see issue8299 for exploration on how fair it is), releasing the programmer from bothering about any bias stemming solely from the engine.

2 of 2
0

I think the problem stems from the fact that, in my opinion, the official documentation is a bit ambiguous on the meaning of Non-Python created threads. Quoting from it:

When threads are created using the dedicated Python APIs (such as the threading module), a thread state is automatically associated to them and the code showed above is therefore correct. However, when threads are created from C (for example by a third-party library with its own thread management), they don’t hold the GIL, nor is there a thread state structure for them.

I have highlighted in bold the parts that I find off-putting. As I have stated in the OP, I am calling PyThread_start_new_thread. Whilst this creates a new thread from C, this function is not part of a third-party library, but of the dedicated Python (C) APIs. Based on this assumption, I ruled out that I actually needed to use the PyGILState_Ensure/PyGILState_Release paradigm.

As far as I can tell from what I've seen with my experiments, a thread created from C with (just) PyThread_start_new_thread should be considered as a non-Python created thread.

🌐
GitHub
github.com › python › cpython › issues › 84179
[C API] Remove PyEval_AcquireLock() and PyEval_ReleaseLock() functions · Issue #84179 · python/cpython
March 17, 2020 - assignee = None closed_at = <Date 2020-03-20.17:31:26.752> created_at = <Date 2020-03-17.23:39:02.379> labels = ['expert-C-API', '3.9'] title = '[C API] Remove PyEval_AcquireLock() and PyEval_ReleaseLock() functions' updated_at = <Date 2020-03-20.17:31:26.751> user = 'https://github.com/vstinner' bugs.python.org fields: activity = <Date 2020-03-20.17:31:26.751> actor = 'vstinner' assignee = 'none' closed = True closed_date = <Date 2020-03-20.17:31:26.752> closer = 'vstinner' components = ['C API'] creation = <Date 2020-03-17.23:39:02.379> creator = 'vstinner' dependencies = [] files = [] hgrep
Author   vstinner
🌐
Narkive
python-dev.python.narkive.com › nyWWm05D › preventing-pyeval-acquirelock-deadlock
Preventing PyEval_AcquireLock deadlock
I'm guessing that you have a C++ library that calls C++ callbacks, and now you want to call a Python callback from your C++ callback. The proper solution is to make sure that you *always* release the Python lock before entering your event loop or anything else that could possibly call callbacks.
🌐
Python
bugs.python.org › issue39998
Issue 39998: [C API] Remove PyEval_AcquireLock() and PyEval_ReleaseLock() functions - Python tracker
March 17, 2020 - This issue tracker has been migrated to GitHub, and is currently read-only. For more information, see the GitHub FAQs in the Python's Developer Guide · This issue has been migrated to GitHub: https://github.com/python/cpython/issues/84179
Top answer
1 of 7
17

Your understanding is correct: invoking PyEval_InitThreads does, among other things, acquire the GIL. In a correctly written Python/C application, this is not an issue because the GIL will be unlocked in time, either automatically or manually.

If the main thread goes on to run Python code, there is nothing special to do, because Python interpreter will automatically relinquish the GIL after a number of instructions have been executed (allowing another thread to acquire it, which will relinquish it again, and so on). Additionally, whenever Python is about to invoke a blocking system call, e.g. to read from the network or write to a file, it will release the GIL around the call.

The original version of this answer pretty much ended here. But there is one more thing to take into account: the embedding scenario.

When embedding Python, the main thread often initializes Python and goes on to execute other, non-Python-related tasks. In that scenario there is nothing that will automatically release the GIL, so this must be done by the thread itself. That is in no way specific to the call that calls PyEval_InitThreads, it is expected of all Python/C code invoked with the GIL acquired.

For example, the main() might contain code like this:

Py_Initialize();
PyEval_InitThreads();

Py_BEGIN_ALLOW_THREADS
... call the non-Python part of the application here ...
Py_END_ALLOW_THREADS

Py_Finalize();

If your code creates threads manually, they need to acquire the GIL before doing anything Python-related, even as simple as Py_INCREF. To do so, use the following:

// Acquire the GIL
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

... call Python code here ...

// Release the GIL. No Python API allowed beyond this point.
PyGILState_Release(gstate);
2 of 7
8

There are two methods of multi threading while executing C/Python API.

1.Execution of different threads with same interpreter - We can execute a Python interpreter and share the same interpreter over the different threads.

The coding will be as follows.

main(){     
//initialize Python
Py_Initialize();
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Main, Today is',ctime(time())\n");

//to Initialize and acquire the global interpreter lock
PyEval_InitThreads();

//release the lock  
PyThreadState *_save;
_save = PyEval_SaveThread();

// Create threads.
for (int i = 0; i<MAX_THREADS; i++)
{   
    hThreadArray[i] = CreateThread
    //(...
        MyThreadFunction,       // thread function name
    //...)

} // End of main thread creation loop.

// Wait until all threads have terminated.
//...
//Close all thread handles and free memory allocations.
//...

//end python here
//but need to check for GIL here too
PyEval_RestoreThread(_save);
Py_Finalize();
return 0;
}

//the thread function

DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
//non Pythonic activity
//...

//check for the state of Python GIL
PyGILState_STATE gilState;
gilState = PyGILState_Ensure();
//execute Python here
PyRun_SimpleString("from time import time,ctime\n"
    "print 'In Thread Today is',ctime(time())\n");
//release the GIL           
PyGILState_Release(gilState);   

//other non Pythonic activity
//...
return 0;
}
  1. Another method is that, we can execute a Python interpreter in the main thread and, to each thread we can give its own sub interpreter. Thus every thread runs with its own separate , independent versions of all imported modules, including the fundamental modules - builtins, __main__ and sys.

The code is as follows

int main()
{

// Initialize the main interpreter
Py_Initialize();
// Initialize and acquire the global interpreter lock
PyEval_InitThreads();
// Release the lock     
PyThreadState *_save;
_save = PyEval_SaveThread();


// create threads
for (int i = 0; i<MAX_THREADS; i++)
{

    // Create the thread to begin execution on its own.

    hThreadArray[i] = CreateThread
    //(...

        MyThreadFunction,       // thread function name
    //...);   // returns the thread identifier 

} // End of main thread creation loop.

  // Wait until all threads have terminated.
WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);

// Close all thread handles and free memory allocations.
// ...


//end python here
//but need to check for GIL here too
//re capture the lock
PyEval_RestoreThread(_save);
//end python interpreter
Py_Finalize();
return 0;
}

//the thread functions
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
// Non Pythonic activity
// ...

//create a new interpreter
PyEval_AcquireLock(); // acquire lock on the GIL
PyThreadState* pThreadState = Py_NewInterpreter();
assert(pThreadState != NULL); // check for failure
PyEval_ReleaseThread(pThreadState); // release the GIL


// switch in current interpreter
PyEval_AcquireThread(pThreadState);

//execute python code
PyRun_SimpleString("from time import time,ctime\n" "print\n"
    "print 'Today is',ctime(time())\n");

// release current interpreter
PyEval_ReleaseThread(pThreadState);

//now to end the interpreter
PyEval_AcquireThread(pThreadState); // lock the GIL
Py_EndInterpreter(pThreadState);
PyEval_ReleaseLock(); // release the GIL

// Other non Pythonic activity
return 0;
}

It is necessary to note that the Global Interpreter Lock still persists and, in spite of giving individual interpreters to each thread, when it comes to python execution, we can still execute only one thread at a time. GIL is UNIQUE to PROCESS, so in spite of providing unique sub interpreter to each thread, we cannot have simultaneous execution of threads

Sources: Executing a Python interpreter in the main thread and, to each thread we can give its own sub interpreter

Multi threading tutorial (msdn)

Find elsewhere
🌐
docs.python.org
docs.python.org › 3 › c-api › init.html
Initialization, finalization, and threads — Python 3.14.3 ...
This page has been split up into the following: Interpreter initialization and finalization, Thread states and the global interpreter lock, Synchronization primitives, Thread-local storage support,...
🌐
The Coding Forums
thecodingforums.com › archive › archive › python
PyThreadState_Swap crash | Python | Coding Forums
April 5, 2011 - I just wanted to point out that PyEval_AcquireLock() and PyEval_ReleaseLock() were recently deprecated: http://bugs.python.org/issue10913 Obviously they'll be around for quite a while longer but given the ominous-but-vague warning in issue10913's description, you might want to stay away from them.
🌐
Oulub
oulub.com › en-US › Python › c-api.init-c.PyEval_AcquireLock
void PyEval_AcquireLock() - Initialization, Fina 3.10 E... - Multilingual Manual - OULUB
Deprecated since version 3.2: This function does not update the current thread state. Please use PyEval_RestoreThread() or PyEval_AcquireThread() instead. ... Calling this function from a thread when the runtime is finalizing will terminate the thread, even if the thread was not created by Python.
🌐
The Coding Forums
thecodingforums.com › archive › archive › python
Multithreaded C API Python questions | Python | Coding Forums
November 17, 2006 - In your/C/system... code you are responsible to release the GIL or not to enable other Python threads (and prevent from deadlocks) Usually you'd do this * if you do time consuming C/system stuff * or if the code can possibly renter Python through the system (e.g. when you call a Windows function which itself can create Windows messages to be routed back into Python message handlers) -robert ... PyGILState_Ensure/Release guarantees to establish the GIL - even if it was already held (useful if you deal with complex call ordering/dependencies) Click to expand... I understand this to mean that I dont need to explicitly lock the GIL (with PyEval_AcquireLock() or PyEval_AcquireThread()).
🌐
Python
docs.python.it › html › api › initialization.html
8. Initialization, Finalization, and Threads
In an application embedding Python, this should be called before using any other Python/C API functions; with the exception of Py_SetProgramName(), PyEval_InitThreads(), PyEval_ReleaseLock(), and PyEval_AcquireLock(). This initializes the table of loaded modules (sys.modules), and creates the ...
🌐
GitHub
github.com › python › cpython › issues › 55122
Deprecate PyEval_AcquireLock() and PyEval_ReleaseLock() · Issue #55122 · python/cpython
assignee = 'https://github.com/pitrou' closed_at = <Date 2011-01-15.13:23:41.360> created_at = <Date 2011-01-15.11:22:12.010> labels = ['type-bug', 'docs'] title = 'Deprecate PyEval_AcquireLock() and PyEval_ReleaseLock()' updated_at = <Date 2011-01-15.13:23:41.359> user = 'https://github.com/pitrou' bugs.python.org fields: activity = <Date 2011-01-15.13:23:41.359> actor = 'pitrou' assignee = 'pitrou' closed = True closed_date = <Date 2011-01-15.13:23:41.360> closer = 'pitrou' components = ['Documentation'] creation = <Date 2011-01-15.11:22:12.010> creator = 'pitrou' dependencies = [] files = [
🌐
GitHub
github.com › python › cpython › issues › 109793
PyThreadState_Swap() During Finalization Causes Immediate Exit (AKA Daemon Threads Are Still the Worst!) · Issue #109793 · python/cpython
September 23, 2023 - During finalization, _PyEval_AcquireLock() immediately causes the thread to exit if the current thread state doesn't match the one that was active when Py_FinalizeEx() was called.
Author   ericsnowcurrently
🌐
University of Houston-Clear Lake
sceweb.uhcl.edu › helm › WEBPAGE-Python › documentation › python_tutorial › api › threads.html
8.1 Thread State and the Global Interpreter Lock
This situation is equivalent to having acquired the lock: when there is only a single thread, all object accesses are safe. Therefore, when this function initializes the lock, it also acquires it. Before the Python thread module creates a new thread, knowing that either it has the lock or the lock hasn't been created yet, it calls PyEval_InitThreads().