A question was posed to me recently:
If you had a thread that produced messages, and pushed those messages one or more consumer threads: how would you write the code to ensure that the producer thread executes as fast as possible? (I.E. no blocking on the producer thread).
An interesting problem to solve! Before looking at the suggested solution below, try and have a go at coming up with your own solution.
…
Let me present one way to go about this problem. The general idea is to use two message queues: a write queue, and a read queue. The writer (producer) thread always writes to the write queue. The reader thread always reads from the read queue. When the reader thread exhausts the read queue, the reader switches the queues: the write queue becomes the read queue, and vice versa. If the reader finds that the new reader queue (after the switch) is also empty, then it waits until the writer thread writes to the write queue. The reader thread then switches the queues again, and reads from the new read queue to extract the most recent item.
Note: I’ve choicely chosen the name “Collection” for this ADT, since the implementation does not guarantee ordering. It is sort of FIFO, but not strictly (otherwise I would of called it a NonBlockingWriterQueue!). Here is a C# example:
public class NonBlockingWriterCollection<T> : IDisposable { private Queue<T> leftQueue = new Queue<T>(); private Queue<T> rightQueue = new Queue<T>(); private volatile bool isWriting = false; private volatile bool writeToLeft = true; private object readerLocker = new object (); private EventWaitHandle readerWaitHandle = new AutoResetEvent (false); private volatile bool disposed = false; ~NonBlockingWriterCollection() { Dispose(false); } #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(!disposed) { disposed = true; // Signal reader to wake up. Since closing/disposing the event handle doesnt raise // object disposed exceptions for threads in waiting states. readerWaitHandle.Set (); if(disposing) { readerWaitHandle.Close (); // Can cause in-progress writes to throw a disposed exception. } } } #endregion /// <summary> /// Enqueues the specified item. /// </summary> /// <param name="item">Item to enqueue</param> /// <exception cref="ObjectDisposedException">If the queue has been disposed prior to executing this methed</exception> /// <remarks> /// Only a single writer thread can enqueue. /// This operation is non-blocking. /// </remarks> public void Write(T item) { if (disposed) throw new ObjectDisposedException ("NonBlockingWriterQueue"); try { isWriting = true; // Queue an item on the write queue if(writeToLeft) leftQueue.Enqueue(item); else rightQueue.Enqueue(item); // Signal reader thread that an item has been added readerWaitHandle.Set(); } finally { isWriting = false; } } /// <summary> /// Dequeues an item. /// </summary> /// <exception cref="ObjectDisposedException">If the queue has been disposed prior-to or during executing this methed</exception> /// <remarks> /// Multiple reader threads can attempt to dequeue an item. /// This operation is blocking (until an item has been enqueued, or the collection has been disposed). /// </remarks> public T Read() { lock (readerLocker) { if (disposed) throw new ObjectDisposedException ("NonBlockingWriterQueue"); // Reset the wait handle, at this point we are searching for an item on either queue readerWaitHandle.Reset (); // Dequeue an item from the queue that is not being written to var readQueue = writeToLeft ? rightQueue : leftQueue; if (readQueue.Count > 0) return readQueue.Dequeue (); while (!disposed) { // The read queue has been exhausted. Swap read/write queue writeToLeft = !writeToLeft; // At this point, the writer thread could be writing to either queue, // wait for the write to finish using a spin lock while (isWriting) { } // busy waiting // Try read again from the read queue readQueue = writeToLeft ? rightQueue : leftQueue; if (readQueue.Count > 0) return readQueue.Dequeue (); // Both lists have been exhausted, we need to wait for the writer to // do something. Block the reader until the writer has signalled. // Note: it may have added an item during the read... so this may // not block, and continue the read right away readerWaitHandle.WaitOne (); } throw new ObjectDisposedException ("NonBlockingWriterQueue"); } } }
If you know you are only going to have a single reader, then you can simply remove the reader mutex to improve performance.
Note the uses of volatile members. Volatile read/writes are required for these primitive members, otherwise all-sorts of chaos could happen should the compiler choose to re-arrange/cache the read/write instructions.
And here is a little test harness:
private const int ReaderCount = 5; private NonBlockingWriterCollection<int> nbQueue; public void Run () { Console.WriteLine ("Starting sim..."); Thread[] readerThreads; Thread writerThread; using(nbQueue = new NonBlockingWriterCollection<int>()) { readerThreads = new Thread[ReaderCount]; for(int i = 0; i < ReaderCount; i++) { readerThreads [i] = new Thread (RunReader); readerThreads [i].Start (i); // box i } writerThread = new Thread (RunWriter); writerThread.Start (); Thread.Sleep (1000 * 5); } Console.WriteLine ("Waiting for sim threads to finish..."); writerThread.Join (); foreach (var rt in readerThreads) rt.Join (); Console.WriteLine ("Finished sim."); } private void RunReader(object threadNum) { var rand = new Random (((int)threadNum) * 6109425); string threadId = "ReaderThread " + (char)('A' + (int)threadNum); // unbox i try { while (true) { var item = nbQueue.Read (); // blocking Console.WriteLine (threadId + " read " + item); Thread.Sleep(rand.Next() % 10); // Do some "work" to process the data } } catch(ObjectDisposedException) {} } private void RunWriter() { string threadId = "WriterThread"; var rand = new Random (); try { while (true) { for(int i = 1; i < rand.Next () % 10; i++) { var item = rand.Next (); Console.WriteLine (threadId + " writing " + item); nbQueue.Write (item); // non-blocking } Thread.Sleep(10 + rand.Next() % 100); // Do some "work" to produce more data } } catch(ObjectDisposedException) {} }
Filed under: Programming Problems - Resolved
