| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [View Raw Code] [Original HTTPS Page] |
This page explains how Python.NET interacts with the Python Global Interpreter Lock (GIL) and with managed threads, and what guarantees the runtime makes when your code is multi-threaded. It covers classic CPython builds for Python 3.11 through 3.15 and the free-threaded builds introduced in CPython 3.13 (Py_GIL_DISABLED). Python.NET supports free-threaded CPython starting with Python 3.14.
Python.NET embeds CPython, so every interaction with a Python object — including reading a PyObject's attributes, calling a Python callable, constructing a Python value, or letting a PyObject go out of scope — must happen while the calling thread is attached to the interpreter. On a classic (GIL-enabled) CPython build "attached" means "holds the GIL"; on a free-threaded build it means "has an active thread state". In both cases the attachment API is the same: Py.GIL() on the C# side and threading.Thread / _thread on the Python side. Forgetting to attach will crash the process or corrupt memory.
When .NET code calls into Python it must hold the GIL. Use the Py.GIL() disposable to acquire and release it:
using (Py.GIL())
{
dynamic np = Py.Import("numpy");
var arr = np.array(new[] { 1, 2, 3 });
// ... interact with arr ...
}
Py.GIL() is re-entrant: nesting calls on the same thread is harmless and cheap. Always pair acquisition with disposal — the using form does this automatically, and you must release the GIL on the same thread that acquired it.
If you need a Python object to outlive the using block, copy what you need (e.g. .As<int[]>() or new PyObject(value)) before releasing the GIL.
If a managed call holds the GIL but then does long-running work that does not touch Python (heavy CPU, blocking I/O, native interop), release the GIL so other Python threads can run:
IntPtr threadState = PythonEngine.BeginAllowThreads();
try
{
DoCpuHeavyWork(); // safe: no Python C API calls
}
finally
{
PythonEngine.EndAllowThreads(threadState);
}
Inside the BeginAllowThreads/EndAllowThreads block you must not touch any Python object. If you need to talk to Python from worker threads spawned in this region, those threads must acquire the GIL themselves with Py.GIL().
Calling a managed method from a Python threading.Thread works transparently — Python.NET handles GIL acquisition/release around the managed call. The managed code sees the GIL held on entry and is free to release it via BeginAllowThreads if it does its own blocking work.
A CLR thread that was not spawned by Python (a thread-pool task, a Thread started in C#, an async continuation that resumed on a different thread, etc.) must acquire the GIL before touching any PyObject:
Task.Run(() =>
{
using (Py.GIL())
{
// safe to use PyObjects here
}
});
Forgetting this is the most common pythonnet threading bug. Symptoms range from immediate segfaults to subtle refcount corruption that crashes much later.
On Linux and macOS, Mono's cooperative/hybrid thread suspension can hang when Python and managed threads interact. If affected, set MONO_THREADS_SUSPEND=preemptive before starting the process, as described in mono/mono#21466. CI uses this workaround for both embedding and Python-hosted tests.
To run the Mono Python tests with per-test output and a Python thread stack dump if a test takes longer than 60 seconds:
MONO_THREADS_SUSPEND=preemptive uv run pytest --runtime mono -v -o faulthandler_timeout=60
PyObject follows the .NET IDisposable pattern. Dispose() (or the end of a using block) drops the underlying Python reference; the GC finalizer queues the same release for the next time Python.NET is on the GIL.
Two practical consequences:
Starting with the free-threaded CPython 3.13+ build (Py_GIL_DISABLED), the GIL is no longer the serialisation point for Python C API calls. Python.NET is tested against the 3.14t (free-threaded) interpreter and behaves as follows under that build:
Behaviour that is unchanged between GIL and free-threaded builds:
Hammering CLR attributes / generic types from many threads is supported:
from threading import Thread
import System
from System.Collections.Generic import List
def worker():
for _ in range(1000):
_ = System.String.Empty
_ = List[int]()
threads = [Thread(target=worker) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
This works on both GIL and free-threaded builds.
If a managed component calls back into a Python delegate from a thread it spawned, that callback path acquires the GIL internally — you do not need to add Py.GIL() around the Python code in the delegate.
If you start a managed thread while holding the GIL and the thread needs to call back into Python, release the GIL first so the new thread can acquire it:
IntPtr threadState;
using (Py.GIL())
{
var pyCallback = scope.Get("on_done");
threadState = PythonEngine.BeginAllowThreads(); // let workers acquire the GIL
try
{
// spawn workers, wait for them...
}
finally
{
PythonEngine.EndAllowThreads(threadState);
}
}
Without the BeginAllowThreads the spawned thread blocks forever waiting for the GIL the parent thread is still holding.
| Back | FazBrowse Home | New Git URL |