Skip to content
Chris Dailey edited this page Feb 28, 2022 · 4 revisions

Threading Support

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.

What to do when calling C# from Python

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);
}

What to do when calling Python from C#

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();

Shutting down from a different thread

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.

Calling into Python

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 = PythonEngine.ImportModule("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}");
}

Example problem cases

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 = PythonEngine.ImportModule("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 = PythonEngine.ImportModule("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.

How this works

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.