0

I'm trying to implement a producer/consumer pattern using BlockingCollection<T> so I've written up a simple console application to test it.

public class Program
{
    public static void Main(string[] args)
    {
        var workQueue = new WorkQueue();
        workQueue.StartProducingItems();
        workQueue.StartProcessingItems();

        while (true)
        {

        }
    }
}

public class WorkQueue
{
    private BlockingCollection<int> _queue;
    private static Random _random = new Random();

    public WorkQueue()
    {
        _queue = new BlockingCollection<int>();

        // Prefill some items.
        for (int i = 0; i < 100; i++)
        {
            //_queue.Add(_random.Next());
        }
    }

    public void StartProducingItems()
    {
        Task.Run(() =>
        {
            _queue.Add(_random.Next()); // Should be adding items to the queue constantly, but instead adds one and then nothing else.
        });
    }

    public void StartProcessingItems()
    {
        Task.Run(() =>
        {
            foreach (var item in _queue.GetConsumingEnumerable())
            {
                Console.WriteLine("Worker 1: " + item);
            }
        });

        Task.Run(() =>
        {
            foreach (var item in _queue.GetConsumingEnumerable())
            {
                Console.WriteLine("Worker 2: " + item);
            }
        });
    }
}

However there are 3 problems with my design:

  • I don't know the correct way of blocking/waiting in my Main method. Doing a simple empty while loop seems terribly inefficient and CPU usage wasting simply for the sake of making sure the application doesn't end.

  • There's also another problem with my design, in this simple application I have a producer that produces items indefinitely, and should never stop. In a real world setup, I'd want it to end eventually (e.g. ran out of files to process). In that case, how should I wait for it to finish in the Main method? Make StartProducingItems async and then await it?

  • Either the GetConsumingEnumerable or Add is not working as I expected. The producer should constantly adding items, but it adds one item and then never adds anymore. This one item is then processed by one of the consumers. Both consumers then block waiting for items to be added, but none are. I know of the Take method, but again spinning on Take in a while loop seems pretty wasteful and inefficient. There is a CompleteAdding method but that then does not allow anything else to ever be added and throws an exception if you try, so that is not suitable.

I know for sure that both consumers are in fact blocking and waiting for new items, as I can switch between threads during debugging:

enter image description here

EDIT:

I've made the changes suggested in one of the comments, but the Task.WhenAll still returns right away.

public Task StartProcessingItems()
{
    var consumers = new List<Task>();

    for (int i = 0; i < 2; i++)
    {
        consumers.Add(Task.Run(() =>
        {
            foreach (var item in _queue.GetConsumingEnumerable())
            {
                Console.WriteLine($"Worker {i}: " + item);
            }
        }));
    }

    return Task.WhenAll(consumers.ToList());
}
mm8
  • 163,881
  • 10
  • 57
  • 88
user9993
  • 5,833
  • 11
  • 56
  • 117
  • `The producer should constantly adding items, but it adds one item and then never adds anymore. This one item is then processed by one of the consumers. Both consumers then block waiting for items to be added, but none are` Well, yes. What else were you expecting? Consumers are blocked until items are added, that's why it's called a blocking collection – Kevin Gosse Apr 09 '17 at 10:33
  • No, the problem is that the producer adds one item and then stops. – user9993 Apr 09 '17 at 10:35
  • That's how you coded it. You're starting a task that adds a single item. Are you a missing a loop or something? – Kevin Gosse Apr 09 '17 at 10:36
  • I'm an idiot. Thanks. – user9993 Apr 09 '17 at 10:37
  • 1
    Haha, it happens all the time when you have your nose in your code. It was so blatant for an external reader that I wasn't sure where you were going at – Kevin Gosse Apr 09 '17 at 10:39

1 Answers1

7

GetConsumingEnumerable() is blocking. If you want to add to the queue constantly, you should put the call to _queue.Add in a loop:

public void StartProducingItems()
{
    Task.Run(() =>
    {
        while (true)
            _queue.Add(_random.Next());
    });
}

Regarding the Main() method you could call the Console.ReadLine() method to prevent the main thread from finishing before you have pressed a key:

public static void Main(string[] args)
{
    var workQueue = new WorkQueue();
    workQueue.StartProducingItems();
    workQueue.StartProcessingItems();

    Console.WriteLine("Press a key to terminate the application...");
    Console.ReadLine();
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • As soon as I press enter, it would end. There has to be a better way. Perhaps the making StartProducingItems should be async? – user9993 Apr 09 '17 at 10:40
  • @user9993 Expose the Task you create in `StartProcessingItems`, then call `.Wait()` on it in the `Main()`. If you have multiple workers, then create an aggregated task with `Task.WhenAll` and expose it instead – Kevin Gosse Apr 09 '17 at 10:42
  • If you expose the Tasks from your WorkQueue class you could wait for these. But how will you tell the producer to stop produce? – mm8 Apr 09 '17 at 10:42
  • Since all your program seems to be doing is to produce and consume items from a BlockingCollection, it doesn't make much sense to await the tasks. You might as well terminating the application as soon as the producer stops producing I guess. – mm8 Apr 09 '17 at 10:56
  • @mm8 `CompleteAdding` must be called when the producer is done, but the consumers need more time to finish processing the last items. So the Wait must be done on the consumers, not the producers – Kevin Gosse Apr 09 '17 at 10:58
  • Interesting. So to expose the tasks to the Main method that were created in the consumer, would I have this signature? `public IEnumerable StartProcessingItems()`? Or did you mean something else? – user9993 Apr 09 '17 at 11:01
  • @user9993 Rather `public Task StartProcessingItems()`. Then do the `Task.WhenAll` inside of the method. Whether there is one or multiple producers should be an implementation detail and shouldn't leak outside of the class – Kevin Gosse Apr 09 '17 at 11:04
  • @KevinGosse Thanks, that makes sense. I've updated my question though as Task.WhenAll seems to be returning right away. – user9993 Apr 09 '17 at 11:19
  • Task.WhenAll returns a Task right away. You should call the Wait() method of this Task. – mm8 Apr 09 '17 at 11:27