- Notifications
You must be signed in to change notification settings - Fork748
Threading
In a multi-threaded environment, it becomes important to manage the PythonGlobal Interpreter Lock(GIL).
When calling Python functions, the caller must hold the GIL. Otherwise, you'lllikely experience crashes withAccessViolationException
or data races, that corrupt memory.
When executing .NET code, consider releasing the GIL to let Python run otherthreads. Otherwise, you might experience deadlocks or starvation.
If you are calling C# from Python, and the C# code performs a long-runningoperation (e.g. computation, I/O, sleep, acquiring a lock, ...), release theGIL viaPythonEngine.BeginAllowThreads()
. Remember to restore it beforereturning control to Python:
voidLongRunningComputation(){varstate=PythonEngine.BeginAllowThreads();Thread.Sleep(1000);PythonEngine.EndAllowThreads(state);}
In a multi-threaded environment, initialize Python from within C# with thefollowing 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 itdown from that thread, special care must be taken. Like above, you may allow yourprocess to shut down on its own, and Python will handle shutting itself down.
However, if you need to shut Python down manually, the call toPythonEngine.EndAllowThreads()
must be omitted. If it is called, the Python runtime won't be able to acquire the GILwhen the call toPythonEngine.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 ausing(Py.GIL())
block:
dynamicm_result;voidFoo(){// Don't access Python up here.using(Py.GIL()){// Safe to access Python here.dynamicmymodule=PyModule.Import("mymodule");dynamicmyfunction=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 tooperate in the background; or when you have multiple C# threads that arecalling into Python.
When embedding C# into Python, imagine calling a version ofLongRunningComputation
above that does not release the GIL:
importDotNetModuleDotNetModule.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'smain thread call:
PythonEngine.Initialize();
But not BeginAllowThreads.
Then, if we have the following Python code:
importtimedefsay_hello():whileTrue:print ("hello")time.sleep(0.1)deflaunch_hello():importthreadinghello_thread=threading.Thread(name="Hello thread",target=say_hello)hello_thread.daemon=Truehello_thread.start()
Which we call from the main thread in C#:
using(Py.GIL()){dynamicmymodule=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'texecuting anything. When the second thread tries to execute this code:
using(Py.GIL()){dynamicosModule=PyModule.Import("mymodule");stringcwd=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 asyou'd expect in a multi-threaded Python application. But when the main thread'scontrol is in C#, Python threads along with C# threads trying to take the GILwill all be blocked.
The Python interpreter only actually runs single-threaded. You can createadditional threads, but only one thread at a time has theGlobal InterpreterLock(GIL),and only that thread can run. Python threads will automatically release the GILwhen they call a blocking system call, or periodically when the interpreter isrunning 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 thatreleases the GIL. Other threads will then be blocked indefinitely from runningPython code. This is the case when you embed Python in a C# application withoutexplicitly releasing the GIL.
When you embed Python in a C# application, if the main thread holds the GIL butoccasionally calls into Python, you will see degraded performance. Other Pythonthreads will get a chance to run occasionally, when the main thread happens tobe running Python code, and at the point the Python interpreter decides to cedecontrol to other Python threads.
The solution above solves the problem by explicitly releasing the GIL when inC#, and only taking it when needed.