12

I have a BlockingCollection. Producer tasks add items to it, and consumer tasks remove items.

Now I want to limit the number of items in the collection, automatically discarding old data if more items are added. The collection should never contain more than the N most recently added items at the same time.

So, if the producers add new items faster than the consumers remove them, I want the consumers to process only the newest items.

I can limit the size of a BlockingCollection in its constructor, but of course that just means it blocks when adding more items, not that it removes old items.

(I do not want blocking on the producer side, only the consumer side should block when retrieving items from an empty collection.)

My current solution is a hack, and only works for a size limit of 1:
(And I am not quite sure it works reliable at all.)

// My consumer task:
foreach (var item in blockingCollection.GetConsumingEnumerable())
{
    var lastItem = item;
    var lastItemTmp = item;
    while (blockingCollection.TryTake(out lastItemTmp))
           lastItem = lastItemTmp;
    // Now lastItem contains the most recent item in the collection, 
    // and older items have been discarded.
    // Proceed consuming lastItem ...
}

Is there a cleaner solution?

Kristof U.
  • 1,263
  • 10
  • 17
HugoRune
  • 13,157
  • 7
  • 69
  • 144
  • What exactly do you mean by "discarding" and by "old" ? "Discarding" might mean "automatically removed from collection" or might mean "not processed". The latter could be handled at the consuming side. "Old" could mean "older than a threshold time period" or "added earlier than the most recently added n items", but also a lot of other things. If you don't define you requirements clearly, it will be difficult to find a good solution. For all I know, you might me looking for a stack. – Kris Vandermotten Feb 20 '14 at 11:33
  • I want the collection to contain only the n most recently added items. Ideally I would like the removal of older items to be automatic, but I am not adverse to modifying producers or consumers to accomplish a similar result. – HugoRune Feb 20 '14 at 12:18
  • @HugoRune: This is a Stack. – Rafa Feb 20 '14 at 12:28
  • @Rafa: I am quite certain that it is not. A stack is an unbounded LIFO structure, with no inherent blocking behaviour. What I am looking for is a structure with the same blocking and tread safety as BlockingCollection (which is by default FIFO), but with the added feature that if it contains more than N items, the older items are silently dropped. It is possible that this can be accomplished with the help of a stack, but I do not quite see how. – HugoRune Feb 20 '14 at 12:36
  • I'm afraid you might have to roll your own. Take a look at http://www.albahari.com/threading/part4.aspx#_Wait_Pulse_Producer_Consumer_Queue for inspiration. You might want to take a look at Reactive Extensions too. – Kris Vandermotten Feb 20 '14 at 14:04

3 Answers3

9

Do it this way:

void AddItemToQueue(MyClass item)
{
    while (!queue.TryAdd(item))
    {
        MyClass trash;
        queue.TryTake(out trash);
    }
}

If the queue is full when you try to add the item, an item is removed from the queue. It uses TryTake because it's possible (unlikely, but possible) that some other thread might have removed the last item from the queue before you get a chance to take one.

This assumes, of course, that you specified a limit on the number of items when you constructed the BlockingCollection.

Another way to do this, although it's more involved, is to create your own circular queue class, and have it implement the IProducerConsumerCollection interface. You can then use an instance of that class as the backing collection for your BlockingCollection. Implementing a circular queue isn't especially difficult, although there are edge cases that are tricky to get right. And you'll have to make it a concurrent data structure, although that's pretty easy to do with a lock.

If you don't expect the queue to overflow often, or if the queue is pretty low traffic (i.e. not being hit thousands of times per second), then my initial suggestion will do what you want and there won't be a performance problem. If there is a performance problem, then the circular queue is the solution.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • 2
    The circular queue solution is not going to work with BlockingCollection. BlockingCollection keeps independent track of how many items are contained n the inner collection and if the inner collection has a different count than what BlockingCollection thinks should be there, exceptions occur. I worked around this once why having the inner queue replace obsolete entries with null and then have the consumer filter those. It works, but it leaks the abstraction to the consumer. – Simon Gillbee Dec 10 '14 at 16:46
0

I would use the Concurrent stack:

Represents a thread-safe last in-first out (LIFO) collection.

http://msdn.microsoft.com/en-us/library/dd267331%28v=vs.110%29.aspx

And I would send in the stack an object that wraps your task adding to it a timestamp. The consumer will take tasks from the stack and discard the ones with a timestamp that is older than a threshold defined by you.

Rafa
  • 2,328
  • 3
  • 27
  • 44
  • A ConcurrentStack could work. Since I want the N most recently added items, a timestamp would not help, I would have to rely on Stack.Count and some sort of heavy locking to discard the right amount of items reliably. My main problem however is that this structure lacks all the blocking behavior of a BlockingCollection, and in particular the BlockingCollection.GetConsumingEnumerable() iterator as well as the CompleteAdding() method, which is the main reason I used this data type. These methods really simplify the implementation of a producer consumer pattern. – HugoRune Feb 20 '14 at 12:42
  • @HugoRune: You're right, I saw no blocking functionality, but it is possible to use some methods to get only a set of items that fulfill a query and discard the rest. It only one idea. :) – Rafa Feb 20 '14 at 12:44
-2

Just call this method before you add your items to it.

public static void Clear<T>(this BlockingCollection<T> blockingCollection)
    {
        if (blockingCollection == null)
        {
            throw new ArgumentNullException("blockingCollection");
        }

        while (blockingCollection.Count > 0)
        {
            T item;
            blockingCollection.TryTake(out item);
        }
    }
Andreas
  • 3,843
  • 3
  • 40
  • 53