--- a PPN by Garber Painting Akron. With Image Size Reduction included!URL: http://github.com/pythonnet/pythonnet/pull/2721.diff
t`` to a managed worker without taking ownership. If
+ the producer disposes its handle while the consumer is still using it,
+ the worker will operate on a freed object. Wrap the producer's
+ ``PyObject`` with ``new PyObject(value)`` before handing it off, or use
+ ``NewReference()``.
+* Calling a Python callable that does CPU-bound work without releasing the
+ GIL. Other Python threads cannot make progress in that case, even on a
+ free-threaded build where the GIL is otherwise a no-op (the callable
+ itself may still touch contended Python state).
diff --git a/src/embed_tests/TestFinalizer.cs b/src/embed_tests/TestFinalizer.cs
index 89dcf137e..d6a3c39e9 100644
--- a/src/embed_tests/TestFinalizer.cs
+++ b/src/embed_tests/TestFinalizer.cs
@@ -30,6 +30,7 @@ private static void FullGCCollect()
{
GC.Collect();
GC.WaitForPendingFinalizers();
+ GC.Collect(); // reclaim objects whose finalizers just ran
}
[Test]
@@ -51,28 +52,28 @@ public void CollectBasicObject()
Finalizer.Instance.BeforeCollect += handler;
IntPtr pyObj = MakeAGarbage(out var shortWeak, out var longWeak);
- FullGCCollect();
- // The object has been resurrected
- Warn.If(
- shortWeak.IsAlive,
- "The referenced object is alive although it should have been collected",
- shortWeak
- );
- Assert.That(
- longWeak.IsAlive,
- Is.True,
- $"The reference object is not alive although it should still be"
- );
+ // The real contract: after the wrapper is GC'd, the underlying
+ // Python pointer must end up in Finalizer's queue. Poll because
+ // .NET Framework / .NET Core differ in how many GC cycles it takes.
+ List garbage = null;
+ for (int attempt = 0; attempt < 10; attempt++)
{
- var garbage = Finalizer.Instance.GetCollectedObjects();
- Assert.NotZero(garbage.Count, "There should still be garbage around");
- Warn.Unless(
- garbage.Contains(pyObj),
- $"The {nameof(longWeak)} reference doesn't show up in the garbage list",
- garbage
- );
+ FullGCCollect();
+ garbage = Finalizer.Instance.GetCollectedObjects();
+ if (garbage.Contains(pyObj)) break;
+ Thread.Sleep(20);
}
+
+ Warn.If(shortWeak.IsAlive,
+ "shortWeak is alive after FullGCCollect; runtime hasn't reclaimed the wrapper yet",
+ shortWeak);
+ // longWeak.IsAlive at this point is .NET-GC-implementation-defined
+ // (Framework reclaims post-finalize objects more eagerly than Core);
+ // intentionally not asserted.
+
+ Assert.That(garbage, Has.Member(pyObj),
+ "PyObject did not reach Finalizer.Instance.GetCollectedObjects()");
try
{
Finalizer.Instance.Collect();
diff --git a/src/embed_tests/TestInterrupt.cs b/src/embed_tests/TestInterrupt.cs
index d48f7c73b..dd3296c54 100644
--- a/src/embed_tests/TestInterrupt.cs
+++ b/src/embed_tests/TestInterrupt.cs
@@ -90,7 +90,17 @@ import time
Assert.That(asyncCall.Wait(TimeSpan.FromSeconds(5)), Is.True, "Async thread was not interrupted in time");
PythonEngine.EndAllowThreads(threadState);
- Assert.That(asyncCall.Result, Is.EqualTo(0));
+ // On free-threaded CPython 3.14, PyRun_SimpleString may return -1 even
+ // when the script catches the async-injected KeyboardInterrupt — the
+ // C-level error indicator depends on which bytecode boundary the
+ // async-exc fires at, and isn't always cleared the way GIL builds clear
+ // it. The interrupt firing and the script terminating cleanly are what
+ // this test exercises; the return code is a side-effect that's only
+ // deterministic under the GIL.
+ if (Python.Runtime.Native.ABI.IsFreeThreaded)
+ Assert.That(asyncCall.Result, Is.AnyOf(0, -1));
+ else
+ Assert.That(asyncCall.Result, Is.EqualTo(0));
}
}
}
diff --git a/src/embed_tests/TestNativeTypeOffset.cs b/src/embed_tests/TestNativeTypeOffset.cs
index 61b6903c5..463fc8cad 100644
--- a/src/embed_tests/TestNativeTypeOffset.cs
+++ b/src/embed_tests/TestNativeTypeOffset.cs
@@ -20,9 +20,10 @@ public class TestNativeTypeOffset
public void LoadNativeTypeOffsetClass()
{
PyObject sys = Py.Import("sys");
- // We can safely ignore the "m" abi flag
+ // "m" is benign; "t" (free-threaded) is handled via ABI.ObjectHeadOffset
+ // rather than the install-time-generated NativeTypeOffset class.
var abiflags = sys.HasAttr("abiflags") ? sys.GetAttr("abiflags").ToString() : "";
- abiflags = abiflags.Replace("m", "");
+ abiflags = abiflags.Replace("m", "").Replace("t", "");
if (!string.IsNullOrEmpty(abiflags))
{
string typeName = "Python.Runtime.NativeTypeOffset, Python.Runtime";
diff --git a/src/embed_tests/TestPyBuffer.cs b/src/embed_tests/TestPyBuffer.cs
index 89ddf9370..2239b6eab 100644
--- a/src/embed_tests/TestPyBuffer.cs
+++ b/src/embed_tests/TestPyBuffer.cs
@@ -1,6 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
+using System.Threading;
using NUnit.Framework;
using Python.Runtime;
using Python.Runtime.Codecs;
@@ -129,6 +130,42 @@ public void MultidimensionalNumPyArray()
});
}
+ [Test]
+ public void ConcurrentDispose()
+ {
+ // Two threads racing on Dispose() must not double-release the view —
+ // Interlocked.Exchange on disposedValue gates PyBuffer_Release.
+ // Smoke test: no crash, exception, or buffer-protocol violation.
+ using var _ = Py.GIL();
+ using var arr = ByteArrayFromAsciiString("hello world! !$%&/()=?");
+
+ const int iterations = 200;
+ for (int i = 0; i < iterations; i++)
+ {
+ PyBuffer buf = arr.GetBuffer();
+
+ IntPtr ts = PythonEngine.BeginAllowThreads();
+ using var barrier = new Barrier(2);
+ Exception captured = null;
+ Action race = () =>
+ {
+ try
+ {
+ barrier.SignalAndWait();
+ using (Py.GIL()) buf.Dispose();
+ }
+ catch (Exception ex) { Interlocked.CompareExchange(ref captured, ex, null); }
+ };
+ var t1 = new Thread(() => race());
+ var t2 = new Thread(() => race());
+ t1.Start(); t2.Start();
+ t1.Join(); t2.Join();
+ PythonEngine.EndAllowThreads(ts);
+
+ if (captured != null) throw captured;
+ }
+ }
+
[MethodImpl(MethodImplOptions.NoInlining)]
static void MakeBufAndLeak(PyObject bufProvider)
{
diff --git a/src/runtime/AssemblyManager.cs b/src/runtime/AssemblyManager.cs
index dcc5aa2f0..344717d01 100644
--- a/src/runtime/AssemblyManager.cs
+++ b/src/runtime/AssemblyManager.cs
@@ -37,7 +37,9 @@ internal class AssemblyManager
// modified from event handlers below, potentially triggered from different .NET threads
private static readonly ConcurrentQueue assemblies = new();
- internal static readonly List pypath = new (capacity: 16);
+ // Snapshot; UpdatePath swaps under lock, readers iterate the captured ref.
+ internal static volatile IReadOnlyList pypath = Array.Empty();
+ private static readonly object _pypathLock = new();
private AssemblyManager()
{
}
@@ -49,7 +51,7 @@ private AssemblyManager()
//github.com/
internal static void Initialize()
{
- pypath.Clear();
+ pypath = Array.Empty();
AppDomain domain = AppDomain.CurrentDomain;
@@ -154,19 +156,20 @@ internal static void UpdatePath()
{
BorrowedReference list = Runtime.PySys_GetObject("path");
var count = Runtime.PyList_Size(list);
- if (count != pypath.Count)
+ if (count == pypath.Count) return;
+
+ lock (_pypathLock)
{
- pypath.Clear();
+ if (count == pypath.Count) return;
+ var fresh = new List(checked((int)count));
probed.Clear();
for (var i = 0; i < count; i++)
{
BorrowedReference item = Runtime.PyList_GetItem(list, i);
string? path = Runtime.GetManagedString(item);
- if (path != null)
- {
- pypath.Add(path);
- }
+ if (path != null) fresh.Add(path);
}
+ pypath = fresh;
}
}
@@ -196,7 +199,8 @@ public static string FindAssembly(string name)
static IEnumerable FindAssemblyCandidates(string name)
{
- foreach (string head in pypath)
+ var paths = pypath;
+ foreach (string head in paths)
{
string path;
if (head == null || head.Length == 0)
diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs
index b884bfa92..7b46e91ad 100644
--- a/src/runtime/ClassManager.cs
+++ b/src/runtime/ClassManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -33,7 +34,11 @@ internal class ClassManager
BindingFlags.Public |
BindingFlags.NonPublic;
- internal static Dictionary cache = new(capacity: 128);
+ // cache: fully-initialised types (lock-free reads).
+ // _inProgressCache: partial types; only accessed under _cacheCreateLock.
+ internal static ConcurrentDictionary cache = new();
+ internal static readonly Dictionary _inProgressCache = new();
+ internal static readonly object _cacheCreateLock = new();
private static readonly Type dtype;
private ClassManager()
@@ -103,13 +108,13 @@ internal static ClassManagerState SaveRuntimeData()
return new()
{
Contexts = contexts,
- Cache = cache,
+ Cache = new Dictionary(cache),
};
}
internal static void RestoreRuntimeData(ClassManagerState storage)
{
- cache = storage.Cache;
+ cache = new ConcurrentDictionary(storage.Cache);
var invalidClasses = new List>();
var contexts = storage.Contexts;
foreach (var pair in cache)
diff --git a/src/runtime/DelegateManager.cs b/src/runtime/DelegateManager.cs
index 4343b9ab7..6efe6603f 100644
--- a/src/runtime/DelegateManager.cs
+++ b/src/runtime/DelegateManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -15,7 +16,9 @@ namespace Python.Runtime
//github.com/
internal class DelegateManager
{
- private readonly Dictionary cache = new();
+ // Lock-free reads; Reflection.Emit (BuildDispatcher) is serialised.
+ private readonly ConcurrentDictionary cache = new();
+ private readonly object _emitLock = new();
private readonly Type basetype = typeof(Dispatcher);
private readonly Type arrayType = typeof(object[]);
private readonly Type voidtype = typeof(void);
@@ -37,18 +40,19 @@ public DelegateManager()
//github.com/
private Type GetDispatcher(Type dtype)
{
- // If a dispatcher type for the given delegate type has already
- // been generated, get it from the cache. The cache maps delegate
- // types to generated dispatcher types. A possible optimization
- // for the future would be to generate dispatcher types based on
- // unique signatures rather than delegate types, since multiple
- // delegate types with the same sig could use the same dispatcher.
-
- if (cache.TryGetValue(dtype, out Type item))
+ if (cache.TryGetValue(dtype, out Type item)) return item;
+ lock (_emitLock)
{
- return item;
+ return cache.TryGetValue(dtype, out item) ? item : BuildDispatcher(dtype);
}
+ }
+ //github.com/
+ //github.com/ Emits a new subclass for
+ //github.com/ and caches it. Must be called under _emitLock.
+ //github.com/
+ private Type BuildDispatcher(Type dtype)
+ {
string name = $"__{dtype.FullName}Dispatcher";
name = name.Replace('.', '_');
name = name.Replace('+', '_');
diff --git a/src/runtime/Finalizer.cs b/src/runtime/Finalizer.cs
index 5b5ecfcfc..b6895e79a 100644
--- a/src/runtime/Finalizer.cs
+++ b/src/runtime/Finalizer.cs
@@ -36,7 +36,8 @@ public ErrorArgs(Exception error)
[DefaultValue(DefaultThreshold)]
public int Threshold { get; set; } = DefaultThreshold;
- bool started;
+ // volatile: ThrottledCollect on PyObject ctor races with Initialize.
+ volatile bool started;
[DefaultValue(true)]
public bool Enable { get; set; } = true;
@@ -113,9 +114,11 @@ internal void ThrottledCollect()
{
if (!started) throw new InvalidOperationException($"{nameof(PythonEngine)} is not initialized");
- _throttled = unchecked(this._throttled + 1);
- if (!started || !Enable || _throttled < Threshold) return;
- _throttled = 0;
+ if (!Enable || Interlocked.Increment(ref _throttled) < Threshold) return;
+ // Defends against externally-driven Py_Finalize (e.g. host's atexit):
+ // queue pointers may already be freed, so skip the drain.
+ if (Runtime._Py_IsFinalizing() == true) return;
+ Interlocked.Exchange(ref _throttled, 0);
this.Collect();
}
@@ -136,7 +139,11 @@ internal void AddFinalizedObject(ref IntPtr obj, int run
return;
}
- Debug.Assert(Runtime.Refcount(new BorrowedReference(obj)) > 0);
+ // Skip on FT: the split refcount can race here and trip the assert spuriously.
+ if (!Native.ABI.IsFreeThreaded)
+ {
+ Debug.Assert(Runtime.Refcount(new BorrowedReference(obj)) > 0);
+ }
#if FINALIZER_CHECK
lock (_queueLock)
diff --git a/src/runtime/InternString.cs b/src/runtime/InternString.cs
index decb3981d..869beb02a 100644
--- a/src/runtime/InternString.cs
+++ b/src/runtime/InternString.cs
@@ -8,6 +8,7 @@ namespace Python.Runtime
{
static partial class InternString
{
+ // Populated only by Initialize (single-threaded); immutable until Shutdown.
private static readonly Dictionary _string2interns = new();
private static readonly Dictionary _intern2strings = new();
const BindingFlags PyIdentifierFieldFlags = BindingFlags.Static | BindingFlags.NonPublic;
diff --git a/src/runtime/Interop.cs b/src/runtime/Interop.cs
index 4aa4aa09b..d98c1693b 100644
--- a/src/runtime/Interop.cs
+++ b/src/runtime/Interop.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -104,7 +105,8 @@ public enum TypeFlags: long
internal class Interop
{
- static readonly Dictionary delegateTypes = new();
+ // Concurrent: type-slot installation can race past TryGetValue.
+ static readonly ConcurrentDictionary delegateTypes = new();
internal static Type GetPrototype(MethodInfo method)
{
@@ -131,7 +133,7 @@ internal static Type GetPrototype(MethodInfo method)
if (invoke.ReturnType != method.ReturnType) continue;
- delegateTypes.Add(method, candidate);
+ delegateTypes.TryAdd(method, candidate);
return candidate;
}
@@ -139,7 +141,9 @@ internal static Type GetPrototype(MethodInfo method)
}
- internal static Dictionary allocatedThunks = new();
+ // Concurrent: documents the multi-writer contract previously enforced
+ // by callers happening to hold TypeManager._cacheCreateLock.
+ internal static ConcurrentDictionary allocatedThunks = new();
internal static ThunkInfo GetThunk(MethodInfo method)
{
diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs
index af75a34b4..383d0bf49 100644
--- a/src/runtime/MethodBinder.cs
+++ b/src/runtime/MethodBinder.cs
@@ -26,8 +26,10 @@ internal class MethodBinder
[NonSerialized]
public MethodBase[]? methods;
+ // volatile + lock: first-time GetMethods() races would otherwise sort `list`
+ // concurrently and publish a partial methods array.
[NonSerialized]
- public bool init = false;
+ public volatile bool init = false;
public const bool DefaultAllowThreads = true;
public bool allow_threads = DefaultAllowThreads;
@@ -189,14 +191,42 @@ internal static MethodInfo[] MatchParameters(MethodBase[] mi, Type[]? tp)
//github.com/
internal MethodBase[] GetMethods()
{
- if (!init)
+ if (init) return methods!;
+ lock (list)
{
- // I'm sure this could be made more efficient.
- list.Sort(new MethodSorter());
- methods = (from method in list where method.Valid select method.Value).ToArray();
+ if (init) return methods!;
+
+ // Filter invalid + precompute precedence (GetParameters allocates) in one
+ // pass so the comparator only does cheap int/type compares O(N log N) times.
+ var pairs = new List>(list.Count);
+ foreach (var m in list)
+ {
+ if (m.Valid) pairs.Add(new(m.Value, GetPrecedence(m.Value)));
+ }
+ if (pairs.Count > 1) pairs.Sort(CompareByDeclaringTypeThenPrecedence);
+
+ var arr = new MethodBase[pairs.Count];
+ for (int i = 0; i < pairs.Count; i++) arr[i] = pairs[i].Key;
+ methods = arr;
init = true;
+ return methods!;
}
- return methods!;
+ }
+
+ //github.com/
+ //github.com/ Sort key for : derived-class methods come before
+ //github.com/ their base, otherwise by precomputed precedence (lower wins).
+ //github.com/
+ private static int CompareByDeclaringTypeThenPrecedence(
+ KeyValuePair a, KeyValuePair b)
+ {
+ Type ta = a.Key.DeclaringType, tb = b.Key.DeclaringType;
+ if (ta != tb)
+ {
+ if (ta.IsAssignableFrom(tb)) return 1;
+ if (tb.IsAssignableFrom(ta)) return -1;
+ }
+ return a.Value.CompareTo(b.Value);
}
//github.com/
@@ -975,54 +1005,6 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
}
- //github.com/
- //github.com/ Utility class to sort method info by parameter type precedence.
- //github.com/
- internal class MethodSorter : IComparer
- {
- int IComparer.Compare(MaybeMethodBase m1, MaybeMethodBase m2)
- {
- MethodBase me1 = m1.UnsafeValue;
- MethodBase me2 = m2.UnsafeValue;
- if (me1 == null && me2 == null)
- {
- return 0;
- }
- else if (me1 == null)
- {
- return -1;
- }
- else if (me2 == null)
- {
- return 1;
- }
-
- if (me1.DeclaringType != me2.DeclaringType)
- {
- // m2's type derives from m1's type, favor m2
- if (me1.DeclaringType.IsAssignableFrom(me2.DeclaringType))
- return 1;
-
- // m1's type derives from m2's type, favor m1
- if (me2.DeclaringType.IsAssignableFrom(me1.DeclaringType))
- return -1;
- }
-
- int p1 = MethodBinder.GetPrecedence(me1);
- int p2 = MethodBinder.GetPrecedence(me2);
- if (p1 < p2)
- {
- return -1;
- }
- if (p1 > p2)
- {
- return 1;
- }
- return 0;
- }
- }
-
-
//github.com/
//github.com/ A Binding is a utility instance that bundles together a MethodInfo
//github.com/ representing a method to call, a (possibly null) target instance for
diff --git a/src/runtime/Native/ABI.cs b/src/runtime/Native/ABI.cs
index c41b42f0a..380bbeac8 100644
--- a/src/runtime/Native/ABI.cs
+++ b/src/runtime/Native/ABI.cs
@@ -7,11 +7,20 @@ namespace Python.Runtime.Native
static class ABI
{
- public static int RefCountOffset { get; } = GetRefCountOffset();
- public static int ObjectHeadOffset => RefCountOffset;
+ // GIL builds only. FT splits the refcount; Refcount uses Py_REFCNT.
+ public static int RefCountOffset { get; private set; }
+
+ // Added to generated TypeOffsets. FT PyObject_HEAD is 16 bytes larger.
+ public static int ObjectHeadOffset { get; private set; }
+
+ public static bool IsFreeThreaded { get; private set; }
internal static void Initialize(Version version)
{
+ IsFreeThreaded = DetectFreeThreaded();
+ RefCountOffset = IsFreeThreaded ? -1 : ProbeRefCountOffset();
+ ObjectHeadOffset = IsFreeThreaded ? 16 : RefCountOffset;
+
string offsetsClassSuffix = string.Format(CultureInfo.InvariantCulture,
"{0}{1}", version.Major, version.Minor);
@@ -34,7 +43,27 @@ internal static void Initialize(Version version)
TypeOffset.Use(typeOffsets, nativeOffsetsClass == null ? ObjectHeadOffset : 0);
}
- static unsafe int GetRefCountOffset()
+ //github.com/
+ //github.com/ True when the running interpreter is a Py_GIL_DISABLED (free-threaded) build.
+ //github.com/
+ static bool DetectFreeThreaded()
+ {
+ // sys._is_gil_enabled() was added in Python 3.13; absent means GIL build.
+ using var sys = Runtime.PyImport_ImportModule("sys");
+ if (sys.IsNull()) { Runtime.PyErr_Clear(); return false; }
+ using var func = Runtime.PyObject_GetAttrString(sys.Borrow(), "_is_gil_enabled");
+ if (func.IsNull()) { Runtime.PyErr_Clear(); return false; }
+ using var args = Runtime.PyTuple_New(0);
+ using var result = Runtime.PyObject_Call(func.Borrow(), args.Borrow(), default);
+ if (result.IsNull()) { Runtime.PyErr_Clear(); return false; }
+ return Runtime.PyObject_IsTrue(result.Borrow()) == 0;
+ }
+
+ //github.com/
+ //github.com/ Locates the offset of ob_refcnt inside PyObject by allocating a
+ //github.com/ fresh list and scanning for its refcount value of 1. GIL builds only.
+ //github.com/
+ static unsafe int ProbeRefCountOffset()
{
using var tempObject = Runtime.PyList_New(0);
IntPtr* tempPtr = (IntPtr*)tempObject.DangerousGetAddress();
diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs
index 264835fff..0b16361ca 100644
--- a/src/runtime/PythonEngine.cs
+++ b/src/runtime/PythonEngine.cs
@@ -16,7 +16,8 @@ namespace Python.Runtime
public class PythonEngine : IDisposable
{
private static DelegateManager? delegateManager;
- private static bool initialized;
+ // volatile: read from worker threads, written from Initialize/Shutdown.
+ private static volatile bool initialized;
private static IntPtr _pythonHome = IntPtr.Zero;
private static IntPtr _programName = IntPtr.Zero;
private static IntPtr _pythonPath = IntPtr.Zero;
@@ -405,7 +406,9 @@ public static void Shutdown()
//github.com/
public delegate void ShutdownHandler();
+ // Lock: ConcurrentStack lacks remove-by-equality; List<> needs serialised mutation.
static readonly List ShutdownHandlers = new();
+ static readonly object _shutdownHandlersLock = new();
//github.com/
//github.com/ Add a function to be called when the engine is shut down.
@@ -422,7 +425,7 @@ public static void Shutdown()
//github.com/
public static void AddShutdownHandler(ShutdownHandler handler)
{
- ShutdownHandlers.Add(handler);
+ lock (_shutdownHandlersLock) ShutdownHandlers.Add(handler);
}
//github.com/
@@ -435,12 +438,15 @@ public static void AddShutdownHandler(ShutdownHandler handler)
//github.com/
public static void RemoveShutdownHandler(ShutdownHandler handler)
{
- for (int index = ShutdownHandlers.Count - 1; index >= 0; --index)
+ lock (_shutdownHandlersLock)
{
- if (ShutdownHandlers[index] == handler)
+ for (int index = ShutdownHandlers.Count - 1; index >= 0; --index)
{
- ShutdownHandlers.RemoveAt(index);
- break;
+ if (ShutdownHandlers[index] == handler)
+ {
+ ShutdownHandlers.RemoveAt(index);
+ break;
+ }
}
}
}
@@ -452,10 +458,17 @@ public static void RemoveShutdownHandler(ShutdownHandler handler)
//github.com/
static void ExecuteShutdownHandlers()
{
- while(ShutdownHandlers.Count > 0)
+ // Invoke unlocked so handlers can re-enter Add/Remove.
+ while (true)
{
- var handler = ShutdownHandlers[ShutdownHandlers.Count - 1];
- ShutdownHandlers.RemoveAt(ShutdownHandlers.Count - 1);
+ ShutdownHandler handler;
+ lock (_shutdownHandlersLock)
+ {
+ if (ShutdownHandlers.Count == 0) return;
+ int last = ShutdownHandlers.Count - 1;
+ handler = ShutdownHandlers[last];
+ ShutdownHandlers.RemoveAt(last);
+ }
handler();
}
}
diff --git a/src/runtime/PythonTypes/PyBuffer.cs b/src/runtime/PythonTypes/PyBuffer.cs
index 120582494..7c1253f5e 100644
--- a/src/runtime/PythonTypes/PyBuffer.cs
+++ b/src/runtime/PythonTypes/PyBuffer.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
namespace Python.Runtime
{
@@ -100,8 +101,7 @@ public static long SizeFromFormat(string format)
//github.com/ C-style (order is 'C') or Fortran-style (order is 'F') contiguous or either one (order is 'A')
public bool IsContiguous(BufferOrderStyle order)
{
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
return Convert.ToBoolean(Runtime.PyBuffer_IsContiguous(ref _view, OrderStyleToChar(order, true)));
}
@@ -111,8 +111,7 @@ public bool IsContiguous(BufferOrderStyle order)
public IntPtr GetPointer(long[] indices)
{
if (indices is null) throw new ArgumentNullException(nameof(indices));
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (Runtime.PyVersion < new Version(3, 7))
throw new NotSupportedException("GetPointer requires at least Python 3.7");
return Runtime.PyBuffer_GetPointer(ref _view, indices.Select(x => checked((nint)x)).ToArray());
@@ -123,8 +122,7 @@ public IntPtr GetPointer(long[] indices)
//github.com/
public void FromContiguous(IntPtr buf, long len, BufferOrderStyle fort)
{
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (Runtime.PyVersion < new Version(3, 7))
throw new NotSupportedException("FromContiguous requires at least Python 3.7");
@@ -139,8 +137,7 @@ public void FromContiguous(IntPtr buf, long len, BufferOrderStyle fort)
//github.com/ Buffer to copy to
public void ToContiguous(IntPtr buf, BufferOrderStyle order)
{
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (Runtime.PyBuffer_ToContiguous(buf, ref _view, _view.len, OrderStyleToChar(order, true)) < 0)
throw PythonException.ThrowLastAsClrException();
@@ -166,8 +163,7 @@ internal static void FillContiguousStrides(int ndims, IntPtr shape, IntPtr strid
//github.com/ On success, set view->obj to a new reference to exporter and return 0. Otherwise, raise PyExc_BufferError, set view->obj to NULL and return -1;
internal void FillInfo(BorrowedReference exporter, IntPtr buf, long len, bool _readonly, int flags)
{
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (Runtime.PyBuffer_FillInfo(ref _view, exporter, buf, (IntPtr)len, Convert.ToInt32(_readonly), flags) < 0)
throw PythonException.ThrowLastAsClrException();
}
@@ -177,8 +173,7 @@ internal void FillInfo(BorrowedReference exporter, IntPtr buf, long len, bool _r
//github.com/
public void Write(byte[] buffer, int sourceOffset, int count, nint destinationOffset)
{
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (_view.ndim != 1)
throw new NotImplementedException("Multidimensional arrays, scalars and objects without a buffer are not supported.");
if (!this.IsContiguous(BufferOrderStyle.C))
@@ -207,8 +202,7 @@ public void Write(byte[] buffer, int sourceOffset, int count, nint destinationOf
//github.com/ Reads the buffer of a python object into a managed byte array. This can be used to pass data like images from python to managed.
//github.com/
public void Read(byte[] buffer, int destinationOffset, int count, nint sourceOffset) {
- if (disposedValue)
- throw new ObjectDisposedException(nameof(PyBuffer));
+ ThrowIfDisposed();
if (_view.ndim != 1)
throw new NotImplementedException("Multidimensional arrays, scalars and objects without a buffer are not supported.");
if (!this.IsContiguous(BufferOrderStyle.C))
@@ -231,31 +225,36 @@ public void Read(byte[] buffer, int destinationOffset, int count, nint sourceOff
Marshal.Copy(_view.buf + sourceOffset, buffer, destinationOffset, count);
}
- private bool disposedValue = false; // To detect redundant calls
+ // 0/1; atomic so finalizer + Dispose cannot double-free the buffer.
+ private int disposedValue;
- private void Dispose(bool disposing)
+ //github.com/
+ //github.com/ Throws if has
+ //github.com/ already released the buffer view.
+ //github.com/
+ private void ThrowIfDisposed()
{
- if (!disposedValue)
- {
- if (Runtime.Py_IsInitialized() == 0)
- throw new InvalidOperationException("Python runtime must be initialized");
+ if (Volatile.Read(ref disposedValue) != 0)
+ throw new ObjectDisposedException(nameof(PyBuffer));
+ }
- // this also decrements ref count for _view->obj
- Runtime.PyBuffer_Release(ref _view);
+ private void Dispose(bool disposing)
+ {
+ if (Interlocked.Exchange(ref disposedValue, 1) != 0) return;
+ if (Runtime.Py_IsInitialized() == 0)
+ throw new InvalidOperationException("Python runtime must be initialized");
- _exporter = null!;
- Shape = null;
- Strides = null;
- SubOffsets = null;
+ // this also decrements ref count for _view->obj
+ Runtime.PyBuffer_Release(ref _view);
- disposedValue = true;
- }
+ _exporter = null!;
+ Shape = null;
+ Strides = null;
+ SubOffsets = null;
}
~PyBuffer()
{
- Debug.Assert(!disposedValue);
-
if (_view.obj != IntPtr.Zero)
{
Finalizer.Instance.AddFinalizedBuffer(ref _view);
diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs
index 1949710fb..3716a99f9 100644
--- a/src/runtime/PythonTypes/PyObject.cs
+++ b/src/runtime/PythonTypes/PyObject.cs
@@ -110,13 +110,21 @@ internal PyObject(in StolenReference reference)
CheckRun();
#endif
- Interlocked.Increment(ref Runtime._collected);
+ // Drop the reference if Python is tearing down; queued Py_DecRef would crash.
+ if (Runtime._Py_IsFinalizing() == true)
+ {
+ rawPtr = IntPtr.Zero;
+ }
+ else
+ {
+ Interlocked.Increment(ref Runtime._collected);
- Finalizer.Instance.AddFinalizedObject(ref rawPtr, run
+ Finalizer.Instance.AddFinalizedObject(ref rawPtr, run
#if TRACE_ALLOC
- , Traceback
+ , Traceback
#endif
- );
+ );
+ }
}
Dispose(false);
diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs
index dc4a4b0a9..169e33eeb 100644
--- a/src/runtime/Runtime.Delegates.cs
+++ b/src/runtime/Runtime.Delegates.cs
@@ -15,6 +15,15 @@ static Delegates()
{
Py_IncRef = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_IncRef), GetUnmanagedDll(_PythonDll));
Py_DecRef = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_DecRef), GetUnmanagedDll(_PythonDll));
+ try
+ {
+ // Exported as a function only on CPython 3.14+; required for free-threaded.
+ Py_REFCNT = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_REFCNT), GetUnmanagedDll(_PythonDll));
+ }
+ catch (MissingMethodException)
+ {
+ Py_REFCNT = null;
+ }
Py_Initialize = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_Initialize), GetUnmanagedDll(_PythonDll));
Py_InitializeEx = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_InitializeEx), GetUnmanagedDll(_PythonDll));
Py_IsInitialized = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(Py_IsInitialized), GetUnmanagedDll(_PythonDll));
@@ -316,6 +325,7 @@ static Delegates()
internal static delegate* unmanaged[Cdecl] Py_IncRef { get; }
internal static delegate* unmanaged[Cdecl] Py_DecRef { get; }
+ internal static delegate* unmanaged[Cdecl] Py_REFCNT { get; }
internal static delegate* unmanaged[Cdecl] Py_Initialize { get; }
internal static delegate* unmanaged[Cdecl] Py_InitializeEx { get; }
internal static delegate* unmanaged[Cdecl] Py_IsInitialized { get; }
diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs
index 399608733..9cf7c8905 100644
--- a/src/runtime/Runtime.cs
+++ b/src/runtime/Runtime.cs
@@ -34,9 +34,10 @@ public static string? PythonDLL
static string? _PythonDll => PythonEnvironment.LibPython;
- private static bool _isInitialized = false;
+ // volatile: read from worker threads, written from Initialize/Shutdown.
+ private static volatile bool _isInitialized = false;
internal static bool IsInitialized => _isInitialized;
- private static bool _typesInitialized = false;
+ private static volatile bool _typesInitialized = false;
internal static bool TypeManagerInitialized => _typesInitialized;
internal static readonly bool Is32Bit = IntPtr.Size == 4;
@@ -53,7 +54,9 @@ public static string? PythonDLL
public static int MainManagedThreadId { get; private set; }
- private static readonly List _pyRefs = new ();
+ // Lock guards re-init from embedders racing on SetPyMember/ResetPyMembers.
+ private static readonly List _pyRefs = new();
+ private static readonly object _pyRefsLock = new();
internal static Version PyVersion
{
@@ -72,7 +75,7 @@ internal static Version PyVersion
internal static int GetRun()
{
- int runNumber = run;
+ int runNumber = Volatile.Read(ref run);
Debug.Assert(runNumber > 0, "This must only be called after Runtime is initialized at least once");
return runNumber;
}
@@ -188,8 +191,8 @@ internal static void Initialize(bool initSigs = false)
static void NewRun()
{
- run++;
- using var pyRun = PyLong_FromLongLong(run);
+ int newRun = Interlocked.Increment(ref run);
+ using var pyRun = PyLong_FromLongLong(newRun);
PySys_SetObject(RunSysPropName, pyRun.BorrowOrThrow());
}
@@ -278,7 +281,7 @@ internal static void Shutdown()
obj: true, derived: false, buffer: false);
CLRObject.creationBlocked = true;
- NullGCHandles(ExtensionType.loadedExtensions);
+ NullGCHandles(ExtensionType.loadedExtensions.Keys);
ClassManager.RemoveClasses();
TypeManager.RemoveTypes();
_typesInitialized = false;
@@ -351,7 +354,7 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops,
}
else if (forceBreakLoops)
{
- NullGCHandles(CLRObject.reflectedObjects);
+ NullGCHandles(CLRObject.reflectedObjects.Keys);
CLRObject.reflectedObjects.Clear();
}
}
@@ -387,14 +390,14 @@ private static void SetPyMember(out PyObject obj, StolenReference value)
throw PythonException.ThrowLastAsClrException();
}
obj = new PyObject(value);
- _pyRefs.Add(obj);
+ lock (_pyRefsLock) _pyRefs.Add(obj);
}
private static void SetPyMemberTypeOf(out PyType obj, PyObject value)
{
var type = PyObject_Type(value);
obj = new PyType(type.StealOrThrow(), prevalidated: true);
- _pyRefs.Add(obj);
+ lock (_pyRefsLock) _pyRefs.Add(obj);
}
private static void SetPyMemberTypeOf(out PyObject obj, StolenReference value)
@@ -411,9 +414,16 @@ private static void SetPyMemberTypeOf(out PyObject obj, StolenReference value)
private static void ResetPyMembers()
{
- foreach (var pyObj in _pyRefs)
+ // Snapshot under lock; Dispose() runs outside it so a callback that
+ // re-enters SetPyMember does not deadlock.
+ PyObject[] snapshot;
+ lock (_pyRefsLock)
+ {
+ snapshot = _pyRefs.ToArray();
+ _pyRefs.Clear();
+ }
+ foreach (var pyObj in snapshot)
pyObj.Dispose();
- _pyRefs.Clear();
}
private static void ClearClrModules()
@@ -607,7 +617,8 @@ internal static unsafe void XIncref(BorrowedReference op)
internal static unsafe void XDecref(StolenReference op)
{
#if DEBUG
- Debug.Assert(op == null || Refcount(new BorrowedReference(op.Pointer)) > 0);
+ // Skip on FT: the split refcount can race here and trip the assert spuriously.
+ Debug.Assert(op == null || Native.ABI.IsFreeThreaded || Refcount(new BorrowedReference(op.Pointer)) > 0);
Debug.Assert(_isInitialized || Py_IsInitialized() != 0 || _Py_IsFinalizing() != false);
#endif
if (op == null) return;
@@ -618,12 +629,10 @@ internal static unsafe void XDecref(StolenReference op)
[Pure]
internal static unsafe nint Refcount(BorrowedReference op)
{
- if (op == null)
- {
- return 0;
- }
- var p = (nint*)(op.DangerousGetAddress() + ABI.RefCountOffset);
- return *p;
+ if (op == null) return 0;
+ // Py_REFCNT is a real symbol on 3.14+; older Pythons expose it as a macro.
+ if (Delegates.Py_REFCNT != null) return Delegates.Py_REFCNT(op);
+ return *(nint*)(op.DangerousGetAddress() + ABI.RefCountOffset);
}
[Pure]
internal static int Refcount32(BorrowedReference op) => checked((int)Refcount(op));
diff --git a/src/runtime/StateSerialization/RuntimeData.cs b/src/runtime/StateSerialization/RuntimeData.cs
index 61e377aa4..c3e5d2287 100644
--- a/src/runtime/StateSerialization/RuntimeData.cs
+++ b/src/runtime/StateSerialization/RuntimeData.cs
@@ -178,7 +178,7 @@ private static SharedObjectsState SaveRuntimeDataObjects()
var contexts = new Dictionary>(PythonReferenceComparer.Instance);
var extensionObjs = new Dictionary(PythonReferenceComparer.Instance);
// make a copy with strongly typed references to avoid concurrent modification
- var extensions = ExtensionType.loadedExtensions
+ var extensions = ExtensionType.loadedExtensions.Keys
.Select(addr => new PyObject(
new BorrowedReference(addr),
// if we don't skip collect, finalizer might modify loadedExtensions
@@ -199,7 +199,7 @@ private static SharedObjectsState SaveRuntimeDataObjects()
var wrappers = new Dictionary