Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2f08c98

Browse files
committed
Atomic type creation in ReflectedClrType.GetOrCreate / TypeManager.GetType
Both type-creation paths had a classic check-then-act race: if (!cache.TryGetValue(t, out var pyType)) { pyType = AllocateClass(t); cache.Add(t, pyType); // throws under contention, partial type otherwise InitializeClass(...); } Two threads racing past the TryGetValue could both call AllocateClass and one would throw on Dictionary.Add ("duplicate key"). Worse, the cache add happens *before* InitializeClass populates members so a third thread's outside-the-lock fast path could observe a partially- initialised type and fail with AttributeError on members not yet added (reproducible under free-threaded Python with concurrent attribute access on built-in CLR types). Convert ClassManager.cache and TypeManager.cache to ConcurrentDictionary and serialise the multi-step initialisation behind a lock. ReflectedClrType.GetOrCreate uses a two-cache design: - `cache` - only fully-initialised types; safe to read on the outside-the-lock fast path. - `_inProgressCache` - partial types being built inside the lock; visible only to the building thread, so self-referential class definitions (which recurse into GetOrCreate for the same type chain) still resolve. Cross-thread access cannot reach the in-progress cache because acquiring the lock is required, so other threads always see fully-ready types. The serialisation snapshot copies remain Dictionary<,> on the wire for binary compatibility.
1 parent 0f24283 commit 2f08c98

3 files changed

Lines changed: 48 additions & 31 deletions

File tree

src/runtime/ClassManager.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ internal class ClassManager
3333
BindingFlags.Public |
3434
BindingFlags.NonPublic;
3535

36-
internal static Dictionary<MaybeType, ReflectedClrType> cache = new(capacity: 128);
36+
// Two-cache design for thread-safe type creation:
37+
// `cache` — only fully-initialised types; safe to read without the lock.
38+
// `_inProgressCache` — partial types being built inside the lock; visible only
39+
// to the building thread (for self-referential definitions).
40+
internal static System.Collections.Concurrent.ConcurrentDictionary<MaybeType, ReflectedClrType> cache = new();
41+
internal static readonly System.Collections.Concurrent.ConcurrentDictionary<MaybeType, ReflectedClrType> _inProgressCache = new();
42+
internal static readonly object _cacheCreateLock = new();
3743
private static readonly Type dtype;
3844

3945
private ClassManager()
@@ -103,13 +109,13 @@ internal static ClassManagerState SaveRuntimeData()
103109
return new()
104110
{
105111
Contexts = contexts,
106-
Cache = cache,
112+
Cache = new Dictionary<MaybeType, ReflectedClrType>(cache),
107113
};
108114
}
109115

110116
internal static void RestoreRuntimeData(ClassManagerState storage)
111117
{
112-
cache = storage.Cache;
118+
cache = new System.Collections.Concurrent.ConcurrentDictionary<MaybeType, ReflectedClrType>(storage.Cache);
113119
var invalidClasses = new List<KeyValuePair<MaybeType, ReflectedClrType>>();
114120
var contexts = storage.Contexts;
115121
foreach (var pair in cache)

src/runtime/TypeManager.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ internal class TypeManager
2626

2727

2828
private const BindingFlags tbFlags = BindingFlags.Public | BindingFlags.Static;
29-
private static readonly Dictionary<MaybeType, PyType> cache = new();
29+
// Thread-safe cache; multi-step creation in GetType is serialised via _cacheCreateLock.
30+
internal static readonly System.Collections.Concurrent.ConcurrentDictionary<MaybeType, PyType> cache = new();
31+
internal static readonly object _cacheCreateLock = new();
3032

3133
static readonly Dictionary<PyType, SlotsHolder> _slotsHolders = new(PythonReferenceComparer.Instance);
3234

@@ -75,7 +77,7 @@ internal static void RemoveTypes()
7577
internal static TypeManagerState SaveRuntimeData()
7678
=> new()
7779
{
78-
Cache = cache,
80+
Cache = new Dictionary<MaybeType, PyType>(cache),
7981
};
8082

8183
internal static void RestoreRuntimeData(TypeManagerState storage)
@@ -94,14 +96,15 @@ internal static void RestoreRuntimeData(TypeManagerState storage)
9496

9597
internal static PyType GetType(Type type)
9698
{
97-
// Note that these types are cached with a refcount of 1, so they
98-
// effectively exist until the CPython runtime is finalized.
99-
if (!cache.TryGetValue(type, out var pyType))
99+
// Cached with refcount 1; effectively lives until the CPython runtime is finalised.
100+
if (cache.TryGetValue(type, out var pyType)) return pyType;
101+
lock (_cacheCreateLock)
100102
{
103+
if (cache.TryGetValue(type, out pyType)) return pyType;
101104
pyType = CreateType(type);
102105
cache[type] = pyType;
106+
return pyType;
103107
}
104-
return pyType;
105108
}
106109
/// <summary>
107110
/// Given a managed Type derived from ExtensionType, get the handle to

src/runtime/Types/ReflectedClrType.cs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,44 @@ internal ReflectedClrType(BorrowedReference original) : base(original) { }
2525
/// </remarks>
2626
public static ReflectedClrType GetOrCreate(Type type)
2727
{
28+
// Fast path: `cache` holds only fully-initialised types.
2829
if (ClassManager.cache.TryGetValue(type, out var pyType))
29-
{
3030
return pyType;
31-
}
3231

33-
try
32+
lock (ClassManager._cacheCreateLock)
3433
{
35-
// Ensure, that matching Python type exists first.
36-
// It is required for self-referential classes
37-
// (e.g. with members, that refer to the same class)
38-
pyType = AllocateClass(type);
39-
ClassManager.cache.Add(type, pyType);
40-
41-
var impl = ClassManager.CreateClass(type);
34+
// Re-check now that we hold the lock; another thread may have finished.
35+
if (ClassManager.cache.TryGetValue(type, out pyType))
36+
return pyType;
37+
// Reentrant call from the same thread (self-referential class) sees the
38+
// partial type that the outer frame allocated.
39+
if (ClassManager._inProgressCache.TryGetValue(type, out pyType))
40+
return pyType;
41+
42+
try
43+
{
44+
pyType = AllocateClass(type);
45+
ClassManager._inProgressCache[type] = pyType;
4246

43-
TypeManager.InitializeClassCore(type, pyType, impl);
47+
var impl = ClassManager.CreateClass(type);
48+
TypeManager.InitializeClassCore(type, pyType, impl);
49+
ClassManager.InitClassBase(type, impl, pyType);
50+
TypeManager.InitializeClass(pyType, impl, type);
4451

45-
ClassManager.InitClassBase(type, impl, pyType);
52+
// Publish the completed type so the fast path can see it.
53+
ClassManager.cache[type] = pyType;
54+
}
55+
catch (Exception e)
56+
{
57+
throw new InternalPythonnetException($"Failed to create Python type for {type.FullName}", e);
58+
}
59+
finally
60+
{
61+
ClassManager._inProgressCache.TryRemove(type, out _);
62+
}
4663

47-
// Now we force initialize the Python type object to reflect the given
48-
// managed type, filling the Python type slots with thunks that
49-
// point to the managed methods providing the implementation.
50-
TypeManager.InitializeClass(pyType, impl, type);
51-
}
52-
catch (Exception e)
53-
{
54-
throw new InternalPythonnetException($"Failed to create Python type for {type.FullName}", e);
64+
return pyType;
5565
}
56-
57-
return pyType;
5866
}
5967

6068
internal void Restore(Dictionary<string, object?> context)

0 commit comments

Comments
 (0)