4

I guess it is sort of a code review, but here is my implementation of the producer / consumer pattern. What I would like to know is would there be a case in which the while loops in the ReceivingThread() or SendingThread() methods might stop executing. Please note that EnqueueSend(DataSendEnqeueInfo info) is called from multiple different threads and I probably can't use tasks here since I definitely have to consume commands in a separate thread.

private Thread mReceivingThread;
private Thread mSendingThread;
private Queue<DataRecievedEnqeueInfo> mReceivingThreadQueue;
private Queue<DataSendEnqeueInfo> mSendingThreadQueue;
private readonly object mReceivingQueueLock = new object();
private readonly object mSendingQueueLock = new object();
private bool mIsRunning;
EventWaitHandle mRcWaitHandle;
EventWaitHandle mSeWaitHandle;

private void ReceivingThread()
{
    while (mIsRunning)
    {
        mRcWaitHandle.WaitOne();
        DataRecievedEnqeueInfo item = null;
        while (mReceivingThreadQueue.Count > 0)
        {
            lock (mReceivingQueueLock)
            {
                item = mReceivingThreadQueue.Dequeue();
            }
            ProcessReceivingItem(item);
        }
        mRcWaitHandle.Reset();
    }
}

private void SendingThread()
{
    while (mIsRunning)
    {
        mSeWaitHandle.WaitOne();
        while (mSendingThreadQueue.Count > 0)
        {
            DataSendEnqeueInfo item = null;
            lock (mSendingQueueLock)
            {
                item = mSendingThreadQueue.Dequeue();
            }
            ProcessSendingItem(item);
        }
        mSeWaitHandle.Reset();
    }
}

internal void EnqueueRecevingData(DataRecievedEnqeueInfo info)
{
    lock (mReceivingQueueLock)
    {
        mReceivingThreadQueue.Enqueue(info);
        mRcWaitHandle.Set();
    }
}

public void EnqueueSend(DataSendEnqeueInfo info)
{
     lock (mSendingQueueLock)
    {
        mSendingThreadQueue.Enqueue(info);
        mSeWaitHandle.Set();
    }
}

P.S the idea here is that am using WaitHandles to put thread to sleep when the queue is empty, and signal them to start when new items are enqueued.

UPDATE I am just going to leave this https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/ ,for people who might be trying to implement Producer/Consumer pattern using TPL or tasks.

Sushant Poojary
  • 353
  • 4
  • 12
  • 1
    You might be better off using a [BlockingCollection](https://msdn.microsoft.com/en-us/library/dd267312(v=vs.110).aspx) which handles all the synchronization logic for you. – Ilian Feb 13 '17 at 05:42
  • Blocking collection is a good choice, however, you may also give a try to [TPL Dataflow](https://msdn.microsoft.com/en-us/library/hh228601.aspx). I can provide some sample logic if needed. – VMAtm Feb 13 '17 at 13:55
  • @VMAtm I've been meaning to wrap my head around TPL dataflow. It would be kind of you if you could provide me with some sample logic, of course, only if its not too much trouble. – Sushant Poojary Feb 13 '17 at 17:36
  • @SushantPoojary I've added the answer with some samples – VMAtm Feb 14 '17 at 18:55
  • Hi Sushant Poojary, please take a look at [this code](https://stackoverflow.com/questions/733793/implementing-the-producer-consumer-pattern-in-c-sharp/47179576#47179576). – sɐunıɔןɐqɐp Nov 08 '17 at 13:22

3 Answers3

5

Use a BlockingCollection instead of Queue, EventWaitHandle and lock objects:

public class DataInfo { }

private Thread mReceivingThread;
private Thread mSendingThread;

private BlockingCollection<DataInfo> queue;

private CancellationTokenSource receivingCts = new CancellationTokenSource();

private void ReceivingThread()
{
    try
    {
        while (!receivingCts.IsCancellationRequested)
        {
            // This will block until an item is added to the queue or the cancellation token is cancelled
            DataInfo item = queue.Take(receivingCts.Token);

            ProcessReceivingItem(item);
        }
    }
    catch (OperationCanceledException)
    {

    }
}

internal void EnqueueRecevingData(DataInfo info)
{
    // When a new item is produced, just add it to the queue
    queue.Add(info);
}

// To cancel the receiving thread, cancel the token
private void CancelReceivingThread()
{
    receivingCts.Cancel();
}
Serge
  • 3,986
  • 2
  • 17
  • 37
  • you have a great example of `BlockingCollection`, can you further elaborate on how you would start this separate thread? And you would add an item to this collection from another thread? – Ozkan May 20 '20 at 07:59
4

Personally, for simple producer-consumer problems, I would just use BlockingCollection. There would be no need to manually code your own synchronization logic. The consuming threads will also block if there are no items present in the queue.

Here is what your code might look like if you use this class:

private BlockingCollection<DataRecievedEnqeueInfo> mReceivingThreadQueue = new BlockingCollection<DataRecievedEnqeueInfo>();
private BlockingCollection<DataSendEnqeueInfo> mSendingThreadQueue = new BlockingCollection<DataSendEnqeueInfo>();

public void Stop()
{
    // No need for mIsRunning. Makes the enumerables in the GetConsumingEnumerable() calls
    // below to complete.
    mReceivingThreadQueue.CompleteAdding();
    mSendingThreadQueue.CompleteAdding();
}

private void ReceivingThread()
{
    foreach (DataRecievedEnqeueInfo item in mReceivingThreadQueue.GetConsumingEnumerable())
    {
        ProcessReceivingItem(item);
    }
}

private void SendingThread()
{
    foreach (DataSendEnqeueInfo item in mSendingThreadQueue.GetConsumingEnumerable())
    {
        ProcessSendingItem(item);
    }
}

internal void EnqueueRecevingData(DataRecievedEnqeueInfo info)
{
    // You can also use TryAdd() if there is a possibility that you
    // can add items after you have stopped. Otherwise, this can throw an
    // an exception after CompleteAdding() has been called.
    mReceivingThreadQueue.Add(info);
}

public void EnqueueSend(DataSendEnqeueInfo info)
{
    mSendingThreadQueue.Add(info);
}
Ilian
  • 5,113
  • 1
  • 32
  • 41
  • If I remember correctly BlockingCollection.GetConsumingEnumerable() doesn't necessarily return the collection in the same order it was enqueued in, which is problem for me. Also I would like the thread to sleep when the collection is empty, which is why am using Waithandles, so is that handled internally too? Also am going to benchmark this, but before I do it, do you know if BlockingCollections perform better or worse? – Sushant Poojary Feb 13 '17 at 06:36
  • `BlockingCollection` will make use of whatever `IProducerConsumerCollection` you give it. By default, it will use `ConcurrentQueue` so it will be processed in the same order it was enqueued in. The same as how you have coded your solution. Perfomance-wise? Only you can profile it. But I would be willing to bet that it would be at least the same or faster, compared to whatever you (or I) will handroll. – Ilian Feb 13 '17 at 06:40
  • Wow! BlockingCollection.GetConsumingEnumerable() automatically blocks the thread.. I don't know why am always afraid to use newer APIs with respect to concurrency and/or threads. -_- . Thank you for your time, I learned something new. – Sushant Poojary Feb 13 '17 at 07:21
1

As suggested in comments, you also can give a try to the TPL Dataflow blocks.

As far as I can see, you have two similar pipelines, for receive and send, so I assume that your class hierarchy is like this:

class EnqueueInfo { }
class DataRecievedEnqeueInfo : EnqueueInfo { }
class DataSendEnqeueInfo : EnqueueInfo { }

We can assemble an abstract class which will encapsulate the logic for creating the pipeline, and providing the interface for processing the items, like this:

abstract class EnqueueInfoProcessor<T>
    where T : EnqueueInfo
{
    // here we will store all the messages received before the handling
    private readonly BufferBlock<T> _buffer;
    // simple action block for actual handling the items
    private ActionBlock<T> _action;

    // cancellation token to cancel the pipeline
    public EnqueueInfoProcessor(CancellationToken token)
    {
        _buffer = new BufferBlock<T>(new DataflowBlockOptions { CancellationToken = token });
        _action = new ActionBlock<T>(item => ProcessItem(item), new ExecutionDataflowBlockOptions
        {
            MaxDegreeOfParallelism = Environment.ProcessorCount,
            CancellationToken = token
        });

        // we are linking two blocks so all the items from buffer
        // will flow down to action block in order they've been received
        _buffer.LinkTo(_action, new DataflowLinkOptions { PropagateCompletion = true });
    }

    public void PostItem(T item)
    {
        // synchronously wait for posting to complete
        _buffer.Post(item);
    }

    public async Task SendItemAsync(T item)
    {
        // asynchronously wait for message to be posted
        await _buffer.SendAsync(item);
    }

    // abstract method to implement
    protected abstract void ProcessItem(T item);
}

Note that you also can encapsulate the link between two blocks by using the Encapsulate<TInput, TOutput> method, but in that case you have to properly handle the Completion of the buffer block, if you're using it.

After this, we just need to implement two methods for receive and send handle logic:

public class SendEnqueueInfoProcessor : EnqueueInfoProcessor<DataSendEnqeueInfo>
{
    SendEnqueueInfoProcessor(CancellationToken token)
        : base(token)
    {

    }
    protected override void ProcessItem(DataSendEnqeueInfo item)
    {
        // send logic here
    }
}

public class RecievedEnqueueInfoProcessor : EnqueueInfoProcessor<DataRecievedEnqeueInfo>
{
    RecievedEnqueueInfoProcessor(CancellationToken token)
        : base(token)
    {

    }
    protected override void ProcessItem(DataRecievedEnqeueInfo item)
    {
        // recieve logic here
    }
}

You also can create more complicated pipeline with TransformBlock<DataRecievedEnqeueInfo, DataSendEnqeueInfo>, if your message flow is about a ReceiveInfo message became SendInfo.

VMAtm
  • 27,943
  • 17
  • 79
  • 125
  • Wow! thank you so for such a detailed example. I've been reading through MSDN about TransformBlock for couple of days and it looks quite interesting. Am going to implement this code and see how it works. – Sushant Poojary Feb 16 '17 at 06:03