I'm trying to implement a simple yet fast producer/consumer collection with the ability to be copied from another thread and to make it as fast as possible, therefore I am not using any locking mechanisms.
Basically the code looks like this (simplified):
var pc = new ProducerConsumer();
pc.Add(1);
var producerTask = Task.Run(() =>
{
var loop = 1;
while (true)
{
pc.Add(loop);
if (pc.count == 5000) // example wraparound
{
loop++;
pc.array[0] = loop;
pc.count = 1; // clear to one element for simplicity of this example
}
}
});
var consumerTask = Task.Run(() =>
{
while (true)
{
Console.WriteLine(pc.ToArray().First());
}
});
Task.WaitAll(producerTask, consumerTask);
class ProducerConsumer
{
public volatile int count = 0;
public readonly int[] array = new int[5000];
// not meant to be thread-safe, designed to be used by just one producer
public void Add(int value)
{
array[count] = value;
count++;
}
// should be thread-safe, used by multiple readers
public int[] ToArray() => array[..count];
}
The idea is that the reader thread(s) should be free to access anything in array[0..count]
at any time and get a valid value. The size of an array is constant.
It seems to be working fine and is 6x faster than ConcurrentBag<T>
in benchmarks but do I have a guarantee that these two lines:
array[count] = value;
count++;
are going to be always called in this specific order and will not get for example optimized into:
array[count++] = value;
by a compiler or CPU?
Do I need Interlocked.MemoryBarrier()
between these two instructions?
Do I need to mark count
as volatile
?
I would like to avoid unnecessary instructions as a simple MemoryBarrier
slows down the loop by 25%. I tested it for some time with all optimizations enabled and on Release without debugger and seems that neither make any difference.
Actually List<T>
from .NET would work great for my needs but for some reason they implemented .Add
as:
_version++;
T[] array = _items;
int size = _size;
if ((uint)size < (uint)array.Length)
{
_size = size + 1;
array[size] = item;
}
else
{
AddWithResize(item);
}
Is there a reason why was it implemented like this? Simple reordering of instructions in lines 6 and 7 could improve thread-safeness of generic List (even though I know it's not meant to be thread-safe).