I am creating an append only data structure that resides in memory and appends records serialized to byte arrays to that memory. I need this to be thread safe and extremely fast so I came up with the following code that is working great so far (this is a pseudo code the actual version is more complex and does some other stuff but just to get the idea)
public sealed class MemoryList : IDisposable
{
private int nextOffset = 0;
private readonly MemoryMappedFile file;
private readonly MemoryMappedViewAccessor va;
public MemoryList(uint capacity)
{
// Some checks on capacity here
var mapName = Guid.NewGuid().ToString("N");
this.file = MemoryMappedFile.CreateNew(mapName, capacity);
this.va = file.CreateViewAccessor(0, capacity);
}
public void AppendMessage(byte[] messagePayload)
{
if (messagePayload == null)
throw new ArgumentNullException(nameof(messagePayload));
if (messagePayload.Length == 0)
throw new ArgumentOutOfRangeException(nameof(messagePayload));
if (TryReserveCapacity(messagePayload.Length, out var offsetToWriteTo))
{
this.va.Write(offsetToWriteTo, messagePayload.Length);
this.va.WriteArray(offsetToWriteTo + sizeof(int), messagePayload, 0, messagePayload.Length);
}
}
private bool TryReserveCapacity(int dataLength, out long reservedOffset)
{
// reserve enough room to store data + its size
var packetSize = sizeof(int) + dataLength;
reservedOffset = Interlocked.Add(ref this.nextOffset, packetSize) - packetSize;
if (this.nextOffset <= this.va.Capacity)
return true;
reservedOffset = -1;
return false;
}
public void Dispose()
{
file?.Dispose();
va?.Dispose();
}
}
This is extremely fast and it works very well. I was not able to break it no matter how hard I tried.
So now what I need is for each appended message the TryReserveCapacity method to output the logical index of each message.
So for the fist message get index 0, for the second - index 1 etc.
This leads to using two calls to Interlocked one for the offset and one for the messageIndex that are apparently not thread safe and I can end up with race conditions leading to the following situation.
MI: 101, Offset: 10000 MI: 100, Offset: 10500
Any ideas on how to guarantee that no MI will be greater than another MI with a bigger offset? All that without using any locks?
So basically how do we change the following method to behave correctly?
private bool TryReserveCapacity(int dataLength, out long reservedOffset, out long messageId)
{
// reserve enough room to store data + its size
var packetSize = sizeof(int) + dataLength;
reservedOffset = Interlocked.Add(ref this.nextOffset, packetSize) - packetSize;
messageId = Interlocked.Increment(ref this.currentMessageId);
if (this.nextOffset <= this.va.Capacity)
return true;
reservedOffset = -1;
return false;
}
P.S I am aware of endianness problems with the example code but as I said just consider it a pseudo code to illustrate the problem.