Example of building a producer/consumer queue in C#.

// -------------------------------------------------------------------------
// WorkTask.cs
// -------------------------------------------------------------------------

using System;

namespace MyNameSpace.ThreadSafeProducerConsumer
{

    /// <summary>
    /// A simple object that will represent the work to be
    /// performed by the consumer threads.
    ///
    /// Represents a task that's added to the work queue by the
    /// producers and retrieved by the consumers to act upon.
    /// </summary>
    /// <remarks>
    /// https://dotnetcodr.com/2015/09/08/using-the-blockingcollection-for-thread-safe-producer-consumer-scenarios-in-net-part-2/
    /// </remarks>
    public class WorkTask
    {
        public WorkTask(string description, DateTime insertedUtc)
        {
            Description = description;
            InsertedUtc = insertedUtc;
        }

        public string Description { get; set; }
        public DateTime InsertedUtc { get; set; }
    }
}

// -------------------------------------------------------------------------
// WorkQueue.cs
// -------------------------------------------------------------------------

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;

namespace MyNameSpace.ThreadSafeProducerConsumer
{
    public class WorkQueue
    {
        private readonly BlockingCollection<WorkTask> _workQueue;

        /// <summary>
        /// An object that holds the work queue itself.
        ///
        /// Constructs a blocking collection with a concurrent queue
        /// as the underlying thread-safe collection.
        ///
        /// This class encapsulates the work queue itself. It will
        /// be the consumer of the work queue, via the
        /// "MonitorWorkQueue" method.
        /// </summary>
        /// <remarks>https://dotnetcodr.com/2015/09/08/using-the-blockingcollection-for-thread-safe-producer-consumer-scenarios-in-net-part-2/</remarks>
        public WorkQueue(IProducerConsumerCollection<WorkTask> workTaskCollection)
        {
            _workQueue = new BlockingCollection<WorkTask>(workTaskCollection);
        }

        public void AddTask(WorkTask workTask)
        {
            _workQueue.Add(workTask);
        }

        /// <summary>
        /// Send a signal to the Consumer that all Producers have
        /// finished adding new items to the Work Queue.
        ///
        /// This is to simulate a scenario where you can determine
        /// in advance, when all expected items have been added to
        /// the work queue.
        ///
        /// The "CompleteAdding()" method marks the collection
        /// as closed and won't accept any more items. Calling
        /// BlockingCollection.Take() will throw an exception
        /// of type InvalidOperationException so we'll need
        /// to catch that.
        /// </summary>
        public void AllItemsAdded()
        {
            _workQueue.CompleteAdding();
        }

        /// <summary>
        /// Monitors the queue.
        ///
        /// The _workQueue.Take() method will block the thread until
        /// there's an element to be retrieved.
        /// </summary>
        public void MonitorWorkQueue()
        {
            while (true)
            {
                try
                {
                    WorkTask wt = _workQueue.Take();

                    System.Console.WriteLine(
                        "Thread '{0}' processing Work Task '{1}', entered on '{2}'.",
                        Thread.CurrentThread.ManagedThreadId,
                        wt.Description, wt.InsertedUtc);
                }
                catch (InvalidOperationException ex)
                {
                    Debug.WriteLine(ex.Message);

                    System.Console.WriteLine(
                        "The Work Queue on Thread '{0}' has been Closed.",
                        Thread.CurrentThread.ManagedThreadId);

                    break;
                }
            }
        }

    }
}

// -------------------------------------------------------------------------
// WorkItemProducer.cs
// -------------------------------------------------------------------------

using System;
using System.Threading;

namespace MyNameSpace.ThreadSafeProducerConsumer
{
    /// <summary>
    /// The producer will need a WorkQueue which it can add
    /// the work items to. It will also need a method to
    /// continuously produce work items.
    ///
    /// We'll let the infinite loop (ProduceWorkItems) sleep for 2 seconds.
    /// </summary>
    /// <remarks>https://dotnetcodr.com/2015/09/09/using-the-blockingcollection-for-thread-safe-producer-consumer-scenarios-in-net-part-3/</remarks>
    public class WorkItemProducer
    {
        private readonly WorkQueue _workQueue;

        public WorkItemProducer(WorkQueue workQueue)
        {
            _workQueue = workQueue;
        }

        /// <summary>
        /// The Producer will generate a random upper limit for
        /// a for-loop.
        ///
        /// The upper limit will indicate the maximum number of
        /// work items to be added to the queue.
        /// </summary>
        public void ProduceWorkItems()
        {
            int upperLimit = new Random().Next(5, 11);

            for (int i = 0; i <= upperLimit; i++)
            {
                Guid jobId = Guid.NewGuid();

                WorkTask wt = new WorkTask(
                    string.Concat("Work with Job ID ", jobId), DateTime.UtcNow);

                System.Console.WriteLine(
                    "Thread '{0}' added work '{1}' at '{2}' to the Work Queue in iteration '{3}'.",
                    Thread.CurrentThread.ManagedThreadId, wt.Description,
                    wt.InsertedUtc, i + 1);

                _workQueue.AddTask(wt);

                Thread.Sleep(2000);
            }
        }
    }
}

// -------------------------------------------------------------------------
// BlockingCollectionSampleService.cs
// -------------------------------------------------------------------------

using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace MyNameSpace.ThreadSafeProducerConsumer
{
    public class BlockingCollectionSampleService
    {
        /// <summary>
        /// The code that runs the threads.
        ///
        /// First, wait for all Producer threads to finish.
        /// Once the Producer threads complete, tell the Consumers
        /// to close.
        /// </summary>
        /// <remarks>https://dotnetcodr.com/2015/09/11/using-the-blockingcollection-for-thread-safe-producer-consumer-scenarios-in-net-part-4/</remarks>
        public void RunBlockingCollectionCodeSample()
        {
            WorkQueue workQueue = new WorkQueue(new ConcurrentQueue<WorkTask>());
            WorkItemProducer producerOne = new WorkItemProducer(workQueue);
            WorkItemProducer producerTwo = new WorkItemProducer(workQueue);
            WorkItemProducer producerThree = new WorkItemProducer(workQueue);

            Task producerTaskOne = Task.Run(() => producerOne.ProduceWorkItems());
            Task producerTaskTwo = Task.Run(() => producerTwo.ProduceWorkItems());
            Task producerTaskThree = Task.Run(() => producerThree.ProduceWorkItems());

            Task consumerTaskOne = Task.Run(() => workQueue.MonitorWorkQueue());
            Task consumerTaskTwo = Task.Run(() => workQueue.MonitorWorkQueue());

            System.Console.WriteLine("> Waiting for Producers to finish...");
            System.Console.WriteLine();
            Task.WaitAll(
                producerTaskOne,
                producerTaskThree,
                producerTaskTwo
            );
            System.Console.WriteLine();
            System.Console.WriteLine("Producers finished.");
            System.Console.WriteLine();

            workQueue.AllItemsAdded();

            System.Console.WriteLine("> Waiting for Consumers to finish...");
            System.Console.WriteLine();
            Task.WaitAll(
                consumerTaskOne,
                consumerTaskTwo
            );
            System.Console.WriteLine();
            System.Console.WriteLine("Consumers finished.");
            System.Console.WriteLine();

            System.Console.WriteLine("Done.");
        }
    }
}

// -------------------------------------------------------------------------
// Program.cs (Example Usage)
// -------------------------------------------------------------------------

using System.Threading;
using System.Threading.Tasks;

namespace MyNameSpace
{
    class Program
    {
        static void Main(string[] args)
        {
            var service = new ThreadSafeProducerConsumer.BlockingCollectionSampleService();
            service.RunBlockingCollectionCodeSample();
        }
}