-
Notifications
You must be signed in to change notification settings - Fork 748
Threading
In a multi-threaded environment, it becomes important to manage the Python Global Interpreter Lock (GIL).
When calling Python functions, the caller must hold the GIL. Otherwise, you'll
likely experience crashes with AccessViolationException
or data races, that corrupt memory.
When executing .NET code, consider releasing the GIL to let Python run other threads. Otherwise, you might experience deadlocks or starvation.
If you are calling C# from Python, and the C# code performs a long-running
operation (e.g. computation, I/O, sleep, acquiring a lock, ...), release the
GIL via PythonEngine.BeginAllowThreads()
. Remember to restore it before
returning control to Python:
void LongRunningComputation() {
var state = PythonEngine.BeginAllowThreads();
Thread.Sleep(1000);
PythonEngine.EndAllowThreads(state);
}
In a multi-threaded environment, initialize Python from within C# with the following code:
PythonEngine.Initialize();
m_threadState = PythonEngine.BeginAllowThreads();
When shutting down, either allow the process to exit on its own, or call:
PythonEngine.EndAllowThreads(m_threadState);
PythonEngine.Shutdown();
If your application initializes Python from one thread but is unable to shut it down from that thread, special care must be taken. Like above, you may allow your process to shut down on its own, and Python will handle shutting itself down.
However, if you need to shut Python down manually, the call to PythonEngine.EndAllowThreads()
must be omitted. If it is called, the Python runtime won't be able to acquire the GIL
when the call to PythonEngine.Shutdown()
is performed, resulting in a deadlock.
In between, when you call into Python, it is critical to acquire the GIL.
The easiest way to achieve this is with a using(Py.GIL())
block:
dynamic m_result;
void Foo() {
// Don't access Python up here.
using(Py.GIL()) {
// Safe to access Python here.
dynamic mymodule = PyModule.Import("mymodule");
dynamic myfunction = mymodule.myfunction;
m_result = myfunction();
}
// The following is unsafe: it is accessing a Python attribute
// without holding the GIL.
Console.Write($"Got the result {m_result.name}");
}
You need to do this when you launch threads in Python and you expect them to operate in the background; or when you have multiple C# threads that are calling into Python.
When embedding C# into Python, imagine calling a version of
LongRunningComputation
above that does not release the GIL:
import DotNetModule
DotNetModule.LongRunningComputation()
All other threads (e.g. GUI threads) would hang for a full second while the C# code sleeps.
On the flip side, when embedding Python into C#, if we have our application's main thread call:
PythonEngine.Initialize();
But not BeginAllowThreads.
Then, if we have the following Python code:
import time
def say_hello():
while True:
print ("hello")
time.sleep(0.1)
def launch_hello():
import threading
hello_thread = threading.Thread(name = "Hello thread", target = say_hello)
hello_thread.daemon = True
hello_thread.start()
Which we call from the main thread in C#:
using (Py.GIL()) {
dynamic mymodule = PyModule.Import("mymodule");
mymodule.launch_hello();
}
We would expect to see "hello" printed immediately and then again every 100ms, but the loop usually won't run right away.
Finally, say in C# we have a second thread while the main thread isn't executing anything. When the second thread tries to execute this code:
using (Py.GIL()) {
dynamic osModule = PyModule.Import("mymodule");
string cwd = osModule.getcwd();
System.Console.Write($"Current directory is: {cwd}");
}
We'll see the second C# thread block before it prints anything.
When the main C# thread invokes Python code, you'll see both cases unblock as you'd expect in a multi-threaded Python application. But when the main thread's control is in C#, Python threads along with C# threads trying to take the GIL will all be blocked.
The Python interpreter only actually runs single-threaded. You can create additional threads, but only one thread at a time has the Global Interpreter Lock (GIL), and only that thread can run. Python threads will automatically release the GIL when they call a blocking system call, or periodically when the interpreter is running Python code.
After the Python interpreter initializes, the main thread holds the GIL.
If a thread is executing C# code while holding the GIL, there's nothing that releases the GIL. Other threads will then be blocked indefinitely from running Python code. This is the case when you embed Python in a C# application without explicitly releasing the GIL.
When you embed Python in a C# application, if the main thread holds the GIL but occasionally calls into Python, you will see degraded performance. Other Python threads will get a chance to run occasionally, when the main thread happens to be running Python code, and at the point the Python interpreter decides to cede control to other Python threads.
The solution above solves the problem by explicitly releasing the GIL when in C#, and only taking it when needed.