Skip to content

Commit a8b253b

Browse files
committed
fix concurrent issues brought up in #1
1 parent 12b3cff commit a8b253b

File tree

2 files changed

+153
-42
lines changed

2 files changed

+153
-42
lines changed

FastCloner.Tests/ConcurrentTests.cs

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Collections.Concurrent;
2+
using FastCloner.Code;
3+
4+
namespace FastCloner.Tests;
5+
6+
[TestFixture]
7+
public class ConcurrentTests
8+
{
9+
private class TestClass
10+
{
11+
public int Value { get; set; }
12+
}
13+
14+
[Test]
15+
public void GenerateCloner_IsCalledOnlyOnce()
16+
{
17+
// Arrange
18+
CountHolder generatorCallCount = new CountHolder();
19+
Type testType = typeof(TestClassForSingleCallTest);
20+
21+
// Act
22+
Task<object>[] tasks = Enumerable.Range(0, 10)
23+
.Select(_ => Task.Run(() =>
24+
FastClonerCache.GetOrAddClass(testType, ValueFactory)))
25+
.ToArray();
26+
27+
Task.WaitAll(tasks);
28+
29+
// Assert
30+
Assert.Multiple(() =>
31+
{
32+
Assert.That(generatorCallCount.Count, Is.EqualTo(1));
33+
34+
object firstResult = tasks[0].Result;
35+
foreach (Task<object> task in tasks)
36+
{
37+
Assert.That(task.Result, Is.SameAs(firstResult));
38+
}
39+
});
40+
41+
return;
42+
43+
object ValueFactory(Type t)
44+
{
45+
Thread.Sleep(100);
46+
generatorCallCount.Increment();
47+
return new Func<object, FastCloneState, object>((obj, state) => obj);
48+
}
49+
}
50+
51+
private class TestClassForSingleCallTest
52+
{
53+
public int Value { get; set; }
54+
}
55+
56+
private class CountHolder
57+
{
58+
private int _count;
59+
public int Count => _count;
60+
61+
public void Increment()
62+
{
63+
Interlocked.Increment(ref _count);
64+
}
65+
}
66+
67+
[Test]
68+
public void CloneObject_WithConcurrentAccess_GeneratesOnlyOneCloner()
69+
{
70+
// Arrange
71+
TestClass obj = new TestClass { Value = 42 };
72+
73+
// Act
74+
Task<TestClass>[] tasks = Enumerable.Range(0, 10).Select(_ => Task.Run(() =>
75+
{
76+
return FastClonerGenerator.CloneObject(obj);
77+
})).ToArray();
78+
79+
Task.WaitAll(tasks);
80+
81+
// Assert
82+
Assert.Multiple(() =>
83+
{
84+
foreach (Task<TestClass> task in tasks)
85+
{
86+
TestClass? clone = task.Result;
87+
Assert.That(clone.Value, Is.EqualTo(42));
88+
}
89+
});
90+
}
91+
92+
[Test]
93+
public void GetOrAdd_CanCallValueFactoryMultipleTimes()
94+
{
95+
// Arrange
96+
ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();
97+
int callCount = 0;
98+
const int key = 1;
99+
100+
// Act
101+
Task<string>[] tasks = Enumerable.Range(0, 10)
102+
.Select(_ => Task.Run(() => dictionary.GetOrAdd(key, ValueFactory)))
103+
.ToArray();
104+
105+
Task.WaitAll(tasks);
106+
107+
// Assert
108+
Assert.Multiple(() =>
109+
{
110+
Assert.That(dictionary, Has.Count.EqualTo(1));
111+
Assert.That(callCount, Is.GreaterThan(1));
112+
Assert.That(tasks.Select(t => t.Result).Distinct().Count(), Is.EqualTo(1));
113+
});
114+
return;
115+
116+
string ValueFactory(int k)
117+
{
118+
Thread.Sleep(100);
119+
Interlocked.Increment(ref callCount);
120+
return $"Value{k}";
121+
}
122+
}
123+
124+
}

FastCloner/Code/FastClonerCache.cs

+29-42
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,52 @@ namespace FastCloner.Code;
44

55
internal static class FastClonerCache
66
{
7-
private static readonly ConcurrentDictionary<Type, object> _typeCache = new ConcurrentDictionary<Type, object>();
8-
9-
private static readonly ConcurrentDictionary<Type, object> _typeCacheDeepTo = new ConcurrentDictionary<Type, object>();
10-
11-
private static readonly ConcurrentDictionary<Type, object> _typeCacheShallowTo = new ConcurrentDictionary<Type, object>();
12-
13-
private static readonly ConcurrentDictionary<Type, object> _structAsObjectCache = new ConcurrentDictionary<Type, object>();
14-
15-
private static readonly ConcurrentDictionary<Tuple<Type, Type>, object> _typeConvertCache = new ConcurrentDictionary<Tuple<Type, Type>, object>();
16-
17-
public static object GetOrAddClass<T>(Type type, Func<Type, T> adder)
7+
private static readonly ConcurrentDictionary<Type, Lazy<object>> classCache = new ConcurrentDictionary<Type, Lazy<object>>();
8+
private static readonly ConcurrentDictionary<Type, Lazy<object>> structCache = new ConcurrentDictionary<Type, Lazy<object>>();
9+
private static readonly ConcurrentDictionary<Type, Lazy<object>> deepClassToCache = new ConcurrentDictionary<Type, Lazy<object>>();
10+
private static readonly ConcurrentDictionary<Type, Lazy<object>> shallowClassToCache = new ConcurrentDictionary<Type, Lazy<object>>();
11+
private static readonly ConcurrentDictionary<Tuple<Type, Type>, Lazy<object>> typeConvertCache = new ConcurrentDictionary<Tuple<Type, Type>, Lazy<object>>();
12+
13+
public static object GetOrAddClass(Type type, Func<Type, object> valueFactory)
1814
{
19-
// return _typeCache.GetOrAdd(type, x => adder(x));
20-
21-
// this implementation is slightly faster than getoradd
22-
if (_typeCache.TryGetValue(type, out object? value)) return value;
23-
24-
value = _typeCache.GetOrAdd(type, t => adder(t));
25-
return value;
15+
Lazy<object> lazy = classCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
16+
return lazy.Value;
2617
}
2718

28-
public static object GetOrAddDeepClassTo<T>(Type type, Func<Type, T> adder)
19+
public static object GetOrAddStructAsObject(Type type, Func<Type, object> valueFactory)
2920
{
30-
if (_typeCacheDeepTo.TryGetValue(type, out object? value)) return value;
31-
32-
value = _typeCacheDeepTo.GetOrAdd(type, t => adder(t));
33-
return value;
21+
Lazy<object> lazy = structCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
22+
return lazy.Value;
3423
}
3524

36-
public static object GetOrAddShallowClassTo<T>(Type type, Func<Type, T> adder)
25+
public static object GetOrAddDeepClassTo(Type type, Func<Type, object> valueFactory)
3726
{
38-
if (_typeCacheShallowTo.TryGetValue(type, out object? value)) return value;
39-
40-
value = _typeCacheShallowTo.GetOrAdd(type, t => adder(t));
41-
return value;
27+
Lazy<object> lazy = deepClassToCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
28+
return lazy.Value;
4229
}
4330

44-
public static object GetOrAddStructAsObject<T>(Type type, Func<Type, T?> adder)
31+
public static object GetOrAddShallowClassTo(Type type, Func<Type, object> valueFactory)
4532
{
46-
// return _typeCache.GetOrAdd(type, x => adder(x));
47-
48-
// this implementation is slightly faster than getoradd
49-
if (_structAsObjectCache.TryGetValue(type, out object? value)) return value;
50-
51-
value = _structAsObjectCache.GetOrAdd(type, t => adder(t));
52-
return value;
33+
Lazy<object> lazy = shallowClassToCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
34+
return lazy.Value;
5335
}
5436

55-
public static T GetOrAddConvertor<T>(Type from, Type to, Func<Type, Type, T> adder) => (T)_typeConvertCache.GetOrAdd(new Tuple<Type, Type>(from, to), (tuple) => adder(tuple.Item1, tuple.Item2));
37+
public static T GetOrAddConvertor<T>(Type from, Type to, Func<Type, Type, T> adder)
38+
{
39+
Tuple<Type, Type> key = new Tuple<Type, Type>(from, to);
40+
Lazy<object> lazy = typeConvertCache.GetOrAdd(key, tuple => new Lazy<object>(() => adder(tuple.Item1, tuple.Item2), LazyThreadSafetyMode.ExecutionAndPublication));
41+
return (T)lazy.Value;
42+
}
5643

5744
/// <summary>
5845
/// This method can be used when we switch between safe / unsafe variants (for testing)
5946
/// </summary>
6047
public static void ClearCache()
6148
{
62-
_typeCache.Clear();
63-
_typeCacheDeepTo.Clear();
64-
_typeCacheShallowTo.Clear();
65-
_structAsObjectCache.Clear();
66-
_typeConvertCache.Clear();
49+
classCache.Clear();
50+
structCache.Clear();
51+
deepClassToCache.Clear();
52+
shallowClassToCache.Clear();
53+
typeConvertCache.Clear();
6754
}
6855
}

0 commit comments

Comments
 (0)