Skip to content

Commit 0860ade

Browse files
committed
Document lock acquisition sites and strong->weak GCHandle swap
Comment-only changes. Adds inline notes at lock acquisitions where the "why" is not obvious from the field declaration alone: - ClassDerivedObject.Reset / GetModuleBuilder: explain that both builder caches must update atomically and that DefineDynamicAssembly / DefineDynamicModule produce duplicates under contention. - TypeManager.GetType: note that CreateType + cache write must be atomic. - ReflectedClrType.GetOrCreate: cross-file lock; mention it also serialises ClassManager.cache and TypeManager._slotsHolders writes. - Runtime.ResetPyMembers: explain the snapshot-then-dispose pattern (Dispose() callbacks would deadlock if invoked under the lock). Expands the strong->weak GCHandle swap in ClassDerivedObject.tp_dealloc to spell out: 1. Why the PyObject is not freed at refcount 0 (C# wrapper may still reference it; ToPython() resurrects via _Py_NewReference). 2. Why the handle is demoted to weak (lets the C# wrapper be GC'd; on collection PyFinalize enqueues the real PyObject_GC_Del). 3. Why the swap uses Interlocked.Exchange (tp_clear may race on the same slot under FT / finalizer thread; without atomic claim both threads could observe and double-free the same handle).
1 parent 92072bd commit 0860ade

4 files changed

Lines changed: 24 additions & 1 deletion

File tree

src/runtime/Runtime.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ private static void SetPyMemberTypeOf(out PyObject obj, StolenReference value)
414414

415415
private static void ResetPyMembers()
416416
{
417+
// Snapshot under lock; Dispose() runs outside it so a callback that
418+
// re-enters SetPyMember does not deadlock.
417419
PyObject[] snapshot;
418420
lock (_pyRefsLock)
419421
{

src/runtime/TypeManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ internal static PyType GetType(Type type)
9999
{
100100
// Cached with refcount 1; effectively lives until the CPython runtime is finalised.
101101
if (cache.TryGetValue(type, out var pyType)) return pyType;
102+
// CreateType + cache write must be atomic so two threads do not both allocate.
102103
lock (_cacheCreateLock)
103104
{
104105
if (cache.TryGetValue(type, out pyType)) return pyType;

src/runtime/Types/ClassDerived.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ static ClassDerivedObject()
4343

4444
public static void Reset()
4545
{
46+
// Atomic replacement of both builder caches so a concurrent
47+
// GetModuleBuilder cannot observe one new + one old.
4648
lock (_buildersLock)
4749
{
4850
assemblyBuilders = new Dictionary<string, AssemblyBuilder>();
@@ -83,7 +85,21 @@ protected override void SetTypeNewSlot(BorrowedReference pyType, SlotsHolder slo
8385
// self may be null after Shutdown begun
8486
if (self is not null)
8587
{
86-
// Atomically swap strong->weak; concurrent tp_clear may also clear the slot.
88+
// Python refcount has reached 0 but we do NOT free the PyObject:
89+
// the C# wrapper (via IPythonDerivedType.__pyobj__) may still
90+
// reference it, and a later ToPython() on that wrapper will
91+
// resurrect a strong handle and call _Py_NewReference.
92+
//
93+
// Demote the GCHandle from strong -> weak so the C# wrapper can
94+
// be collected; when it is, PyFinalize enqueues the actual
95+
// PyObject_GC_Del. Until then the slot keeps the wrapper
96+
// reachable from Python -> C# but not the other way.
97+
//
98+
// Interlocked.Exchange: under FT (or .NET-finalizer-thread
99+
// races) tp_clear can hit the same slot. Whichever thread
100+
// observes the original handle frees it; the loser frees the
101+
// weak handle it just allocated. Without the atomic swap both
102+
// threads could see and free the same handle (double-free).
87103
GCHandle weak = GCHandle.Alloc(self, GCHandleType.Weak);
88104
BorrowedReference borrow = ob.Borrow();
89105
int offset = Util.ReadInt32(Runtime.PyObject_TYPE(borrow), Offsets.tp_clr_inst_offset);
@@ -678,6 +694,8 @@ private static void AddPythonProperty(string propertyName, PyObject func, TypeBu
678694
private static ModuleBuilder GetModuleBuilder(string assemblyName, string moduleName)
679695
{
680696
var key = Tuple.Create(assemblyName, moduleName);
697+
// Cache check-and-create must be atomic; DefineDynamicAssembly /
698+
// DefineDynamicModule produce duplicate builders under contention.
681699
lock (_buildersLock)
682700
{
683701
if (moduleBuilders.TryGetValue(key, out ModuleBuilder? existing))

src/runtime/Types/ReflectedClrType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public static ReflectedClrType GetOrCreate(Type type)
2929
if (ClassManager.cache.TryGetValue(type, out var pyType))
3030
return pyType;
3131

32+
// Shared with ClassManager.cache + TypeManager._slotsHolders writes
33+
// so the multi-step type build below is atomic.
3234
lock (ClassManager._cacheCreateLock)
3335
{
3436
// Re-check now that we hold the lock; another thread may have finished.

0 commit comments

Comments
 (0)