For a while now I've been after a lock-free, simple and scalable implementation of a multiple producer, single consumer queue for delegates in C#. I think I finally have it. I've run basic tests on it showing it works, and the design is so simple that I've managed to convince myself it is rock-solid.
This relies on a compare-and-swap approach to update the queue, similar to the new lock-free pattern used to generate event field accessors in C# 4.0 (see here), combined with an Interlocked.Exchange to read-and-set the queue to null.
Essentially, this derived from the realization that message-queues are really one-shot multicast delegates that reset their invocation lists after message execution!
However, parallel code is very hard to get right so I would like confirmation that this pattern is indeed correct. I've found my intuitions can be surprisingly misleading when it comes to parallelism, and there's always some crazy edge-case driving me off...
So, to the question: Can anyone confirm to me that the below message queue design pattern is thread-safe?
public class MessageQueue
{
Action queue;
public void Enqueue(Action message)
{
Action currentQueue;
var previousQueue = queue;
do
{
currentQueue = previousQueue;
var newQueue = currentQueue + message;
previousQueue = Interlocked.CompareExchange(ref queue, newQueue, currentQueue);
}
while (previousQueue != currentQueue);
}
public void Process()
{
var current = Interlocked.Exchange(ref queue, null);
if (current != null)
{
current();
}
}
}
previousQueue = Volatile.Read(queue)
? The interaction between Interlocked and Volatile has always left me a bit confused, ever since (in c# 2.0) passing avolatile int
to anInterlocked
method would generate a compiler warning. \$\endgroup\$previousQueue != currentQueue
check should catch the problem. \$\endgroup\$