We have a method which maintains global sequence index of all events in our application. As it is website, it is expected to have such method thread safe. Thread-safe implementation was following:
private static long lastUsedIndex = -1;
public static long GetNextIndex()
{
Interlocked.Increment(ref lastUsedIndex);
return lastUsedIndex;
}
However we noticed that under some not heavy load duplicated indexes appeared in system. Simple test shown that there are about of 1500 duplicates for 100000 iterations.
internal class Program
{
private static void Main(string[] args)
{
TestInterlockedIncrement.Run();
}
}
internal class TestInterlockedIncrement
{
private static long lastUsedIndex = -1;
public static long GetNextIndex()
{
Interlocked.Increment(ref lastUsedIndex);
return lastUsedIndex;
}
public static void Run()
{
var indexes = Enumerable
.Range(0, 100000)
.AsParallel()
.WithDegreeOfParallelism(32)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.Select(_ => GetNextIndex())
.ToList();
Console.WriteLine($"Total values: {indexes.Count}");
Console.WriteLine($"Duplicate values: {indexes.GroupBy(i => i).Count(g => g.Count() > 1)}");
}
}
This can be fixed with following implementation:
public static long GetNextIndex()
{
return Interlocked.Increment(ref lastUsedIndex);
}
However, I do not clearly understand, why first implementation did not work. Can anybody help me describing what is happening in that case?
lastUsedIndex
could have been updated by another call toInterlocked.Increment
in between getting the result and returning it to the caller. – Escharoticreturn lastUsedIndex;
Small odds, not zero. The code does[read modify write] read
. The atomicity guarantee you get from Interlocked only lasts for the operations in the brackets. You'd have to uselock [read modify write read]
to make it safe. – Surovy