Threadsafety and Interlocked.CompareExchange
Hey, i have written a method that updates a dictionary without locks. Of course i have written a test for it and it succeeded for 15 runs. But today i executed all tests and suddendly the test failed, which i cannot reproduce a second time. Can you guys spot what i might have done wrong?
Test failure:
class UpdateQueueStatus
{
private static Dictionary<string, UpdateQueueStatus> _mappings = new(StringComparer.OrdinalIgnoreCase);
public static readonly UpdateQueueStatus None = Create(0);
public static readonly UpdateQueueStatus Pending = Create(1);
public static readonly UpdateQueueStatus Queued = Create(0);
public static readonly UpdateQueueStatus Enqueued = Create(1);
public static readonly UpdateQueueStatus Caching = Create(2);
public static readonly UpdateQueueStatus Cached = Create(3);
public static readonly UpdateQueueStatus Applying = Create(4);
public static readonly UpdateQueueStatus Applied = Create(5);
public static readonly UpdateQueueStatus Completed = Create(6);
public static readonly UpdateQueueStatus Failed = Create(7);
public static readonly UpdateQueueStatus Cancelled = Create(8);
public static readonly UpdateQueueStatus Downloading = Create(2);
public string Value { get; }
public UpdateQueueStatus(string value)
{
Value = value;
}
public static UpdateQueueStatus Parse(string value)
{
if (_mappings.TryGetValue(value, out var status))
{
return status;
}
var result = new UpdateQueueStatus(value);
while (true)
{
var currentMap = _mappings;
var newMap = new Dictionary<string, UpdateQueueStatus>(currentMap, StringComparer.OrdinalIgnoreCase);
newMap.TryAdd(value, result);
if (Interlocked.CompareExchange(ref _mappings, newMap, currentMap) == currentMap)
{
break;
}
}
return result;
}
private static UpdateQueueStatus Create(int number, [CallerMemberName] string? name = null)
{
var result = new UpdateQueueStatus(name ?? "None");
_mappings.TryAdd(number.ToString(), result);
_mappings.TryAdd(result.Value, result);
_intMappings.Add((result.Value, number));
return result;
}
}
// Test:
public class Test
{
[Fact]
public void Should_Be_Threadsafe()
{
var count = UpdateQueueStatus.GetMappings().Count;
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
var task = Task.Run(() =>
{
for (int j = 0; j < 100; j++)
{
UpdateQueueStatus.Parse("value" + j);
}
});
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray());
//Console.WriteLine("Number of keys in the dictionary: " + UpdateQueueStatus.GetMappings());
Assert.Equal(count + 100, UpdateQueueStatus.GetMappings().Count);
}
}class UpdateQueueStatus
{
private static Dictionary<string, UpdateQueueStatus> _mappings = new(StringComparer.OrdinalIgnoreCase);
public static readonly UpdateQueueStatus None = Create(0);
public static readonly UpdateQueueStatus Pending = Create(1);
public static readonly UpdateQueueStatus Queued = Create(0);
public static readonly UpdateQueueStatus Enqueued = Create(1);
public static readonly UpdateQueueStatus Caching = Create(2);
public static readonly UpdateQueueStatus Cached = Create(3);
public static readonly UpdateQueueStatus Applying = Create(4);
public static readonly UpdateQueueStatus Applied = Create(5);
public static readonly UpdateQueueStatus Completed = Create(6);
public static readonly UpdateQueueStatus Failed = Create(7);
public static readonly UpdateQueueStatus Cancelled = Create(8);
public static readonly UpdateQueueStatus Downloading = Create(2);
public string Value { get; }
public UpdateQueueStatus(string value)
{
Value = value;
}
public static UpdateQueueStatus Parse(string value)
{
if (_mappings.TryGetValue(value, out var status))
{
return status;
}
var result = new UpdateQueueStatus(value);
while (true)
{
var currentMap = _mappings;
var newMap = new Dictionary<string, UpdateQueueStatus>(currentMap, StringComparer.OrdinalIgnoreCase);
newMap.TryAdd(value, result);
if (Interlocked.CompareExchange(ref _mappings, newMap, currentMap) == currentMap)
{
break;
}
}
return result;
}
private static UpdateQueueStatus Create(int number, [CallerMemberName] string? name = null)
{
var result = new UpdateQueueStatus(name ?? "None");
_mappings.TryAdd(number.ToString(), result);
_mappings.TryAdd(result.Value, result);
_intMappings.Add((result.Value, number));
return result;
}
}
// Test:
public class Test
{
[Fact]
public void Should_Be_Threadsafe()
{
var count = UpdateQueueStatus.GetMappings().Count;
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
var task = Task.Run(() =>
{
for (int j = 0; j < 100; j++)
{
UpdateQueueStatus.Parse("value" + j);
}
});
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray());
//Console.WriteLine("Number of keys in the dictionary: " + UpdateQueueStatus.GetMappings());
Assert.Equal(count + 100, UpdateQueueStatus.GetMappings().Count);
}
}Test failure:
The test failed:
Message:
Assert.Equal() Failure
Expected: 125
Actual: 121The test failed:
Message:
Assert.Equal() Failure
Expected: 125
Actual: 121