Skip to content

Commit

Permalink
fix concurrent issues brought up in #1
Browse files Browse the repository at this point in the history
  • Loading branch information
lofcz committed Jan 8, 2025
1 parent 12b3cff commit a8b253b
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 42 deletions.
124 changes: 124 additions & 0 deletions FastCloner.Tests/ConcurrentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Collections.Concurrent;
using FastCloner.Code;

namespace FastCloner.Tests;

[TestFixture]
public class ConcurrentTests
{
private class TestClass
{
public int Value { get; set; }
}

[Test]
public void GenerateCloner_IsCalledOnlyOnce()
{
// Arrange
CountHolder generatorCallCount = new CountHolder();
Type testType = typeof(TestClassForSingleCallTest);

// Act
Task<object>[] tasks = Enumerable.Range(0, 10)
.Select(_ => Task.Run(() =>
FastClonerCache.GetOrAddClass(testType, ValueFactory)))
.ToArray();

Task.WaitAll(tasks);

// Assert
Assert.Multiple(() =>
{
Assert.That(generatorCallCount.Count, Is.EqualTo(1));

object firstResult = tasks[0].Result;
foreach (Task<object> task in tasks)
{
Assert.That(task.Result, Is.SameAs(firstResult));
}
});

return;

object ValueFactory(Type t)
{
Thread.Sleep(100);
generatorCallCount.Increment();
return new Func<object, FastCloneState, object>((obj, state) => obj);
}
}

private class TestClassForSingleCallTest
{
public int Value { get; set; }
}

private class CountHolder
{
private int _count;
public int Count => _count;

public void Increment()
{
Interlocked.Increment(ref _count);
}
}

[Test]
public void CloneObject_WithConcurrentAccess_GeneratesOnlyOneCloner()
{
// Arrange
TestClass obj = new TestClass { Value = 42 };

// Act
Task<TestClass>[] tasks = Enumerable.Range(0, 10).Select(_ => Task.Run(() =>
{
return FastClonerGenerator.CloneObject(obj);
})).ToArray();

Task.WaitAll(tasks);

// Assert
Assert.Multiple(() =>
{
foreach (Task<TestClass> task in tasks)
{
TestClass? clone = task.Result;
Assert.That(clone.Value, Is.EqualTo(42));
}
});
}

[Test]
public void GetOrAdd_CanCallValueFactoryMultipleTimes()
{
// Arrange
ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();
int callCount = 0;
const int key = 1;

// Act
Task<string>[] tasks = Enumerable.Range(0, 10)
.Select(_ => Task.Run(() => dictionary.GetOrAdd(key, ValueFactory)))
.ToArray();

Task.WaitAll(tasks);

// Assert
Assert.Multiple(() =>
{
Assert.That(dictionary, Has.Count.EqualTo(1));
Assert.That(callCount, Is.GreaterThan(1));
Assert.That(tasks.Select(t => t.Result).Distinct().Count(), Is.EqualTo(1));
});
return;

string ValueFactory(int k)
{
Thread.Sleep(100);
Interlocked.Increment(ref callCount);
return $"Value{k}";
}
}

}
71 changes: 29 additions & 42 deletions FastCloner/Code/FastClonerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,52 @@ namespace FastCloner.Code;

internal static class FastClonerCache
{
private static readonly ConcurrentDictionary<Type, object> _typeCache = new ConcurrentDictionary<Type, object>();

private static readonly ConcurrentDictionary<Type, object> _typeCacheDeepTo = new ConcurrentDictionary<Type, object>();

private static readonly ConcurrentDictionary<Type, object> _typeCacheShallowTo = new ConcurrentDictionary<Type, object>();

private static readonly ConcurrentDictionary<Type, object> _structAsObjectCache = new ConcurrentDictionary<Type, object>();

private static readonly ConcurrentDictionary<Tuple<Type, Type>, object> _typeConvertCache = new ConcurrentDictionary<Tuple<Type, Type>, object>();

public static object GetOrAddClass<T>(Type type, Func<Type, T> adder)
private static readonly ConcurrentDictionary<Type, Lazy<object>> classCache = new ConcurrentDictionary<Type, Lazy<object>>();
private static readonly ConcurrentDictionary<Type, Lazy<object>> structCache = new ConcurrentDictionary<Type, Lazy<object>>();
private static readonly ConcurrentDictionary<Type, Lazy<object>> deepClassToCache = new ConcurrentDictionary<Type, Lazy<object>>();
private static readonly ConcurrentDictionary<Type, Lazy<object>> shallowClassToCache = new ConcurrentDictionary<Type, Lazy<object>>();
private static readonly ConcurrentDictionary<Tuple<Type, Type>, Lazy<object>> typeConvertCache = new ConcurrentDictionary<Tuple<Type, Type>, Lazy<object>>();

public static object GetOrAddClass(Type type, Func<Type, object> valueFactory)
{
// return _typeCache.GetOrAdd(type, x => adder(x));

// this implementation is slightly faster than getoradd
if (_typeCache.TryGetValue(type, out object? value)) return value;

value = _typeCache.GetOrAdd(type, t => adder(t));
return value;
Lazy<object> lazy = classCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
return lazy.Value;
}

public static object GetOrAddDeepClassTo<T>(Type type, Func<Type, T> adder)
public static object GetOrAddStructAsObject(Type type, Func<Type, object> valueFactory)
{
if (_typeCacheDeepTo.TryGetValue(type, out object? value)) return value;

value = _typeCacheDeepTo.GetOrAdd(type, t => adder(t));
return value;
Lazy<object> lazy = structCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
return lazy.Value;
}

public static object GetOrAddShallowClassTo<T>(Type type, Func<Type, T> adder)
public static object GetOrAddDeepClassTo(Type type, Func<Type, object> valueFactory)
{
if (_typeCacheShallowTo.TryGetValue(type, out object? value)) return value;

value = _typeCacheShallowTo.GetOrAdd(type, t => adder(t));
return value;
Lazy<object> lazy = deepClassToCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
return lazy.Value;
}

public static object GetOrAddStructAsObject<T>(Type type, Func<Type, T?> adder)
public static object GetOrAddShallowClassTo(Type type, Func<Type, object> valueFactory)
{
// return _typeCache.GetOrAdd(type, x => adder(x));

// this implementation is slightly faster than getoradd
if (_structAsObjectCache.TryGetValue(type, out object? value)) return value;

value = _structAsObjectCache.GetOrAdd(type, t => adder(t));
return value;
Lazy<object> lazy = shallowClassToCache.GetOrAdd(type, t => new Lazy<object>(() => valueFactory(t), LazyThreadSafetyMode.ExecutionAndPublication));
return lazy.Value;
}

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));
public static T GetOrAddConvertor<T>(Type from, Type to, Func<Type, Type, T> adder)
{
Tuple<Type, Type> key = new Tuple<Type, Type>(from, to);
Lazy<object> lazy = typeConvertCache.GetOrAdd(key, tuple => new Lazy<object>(() => adder(tuple.Item1, tuple.Item2), LazyThreadSafetyMode.ExecutionAndPublication));
return (T)lazy.Value;
}

/// <summary>
/// This method can be used when we switch between safe / unsafe variants (for testing)
/// </summary>
public static void ClearCache()
{
_typeCache.Clear();
_typeCacheDeepTo.Clear();
_typeCacheShallowTo.Clear();
_structAsObjectCache.Clear();
_typeConvertCache.Clear();
classCache.Clear();
structCache.Clear();
deepClassToCache.Clear();
shallowClassToCache.Clear();
typeConvertCache.Clear();
}
}

0 comments on commit a8b253b

Please sign in to comment.