Home > Articles > Programming > C#

.NET Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

The BlockingCollection Class

Last updated Mar 14, 2003.

At first look, the new BlockingCollection looks useful, but not hugely important. But after a bit of study, you begin to appreciate all it can do. The MSDN documentation says that BlockingCollection "Provides blocking and bounding capabilities for thread-safe collections that implement IProducerConsumerCollection<T>." Let's take a look first at the blocking and bounding capabilities. Then we'll look at where the IProducerConsumerCollection interface comes in.

You can use BlockingCollection in two ways: with or without a backing store. If you don't specify a backing store, then BlockingCollection uses a ConcurrentQueue for the backing store, and the resulting collection behaves much like a ConcurrentQueue with a slightly different interface. Those differences, though, are very important.

The "bounded" part of the description is simple: you can set an upper limit on the number of items to be stored in the collection. This is very important if you're processing more data than will fit in memory. For example, I have a program that on a daily basis merges about 100 million transactions into an existing repository. The main processing loop for that program can be described like this:

while not end of repository
    read next repository record
    read all transactions for this record
    add transactions to record
    write modified repository record
end while

Rather than making the main loop wait on file input when reading the transactions, I have a separate thread reading from the disk and placing transactions into a queue so that the main loop gets transactions from the queue very quickly. Doing it this way almost doubles the program's speed. I do the same kind of thing when reading records from the repository and writing modified records to the new repository.

The thread that reads transactions from disk can fill the queue a whole lot faster than the main processing loop can empty it, and if I let it go it would fill memory with transactions and then the program would crash with an out of memory exception. Such a situation is commonly referred to as "unbounded memory usage."

To prevent unbounded memory usage, BlockingCollection lets you set the maximum number of items to be held in the collection. An attempt to add another item will block until there is space in the collection to hold it. BlockingCollection takes care of the complicated code required for waiting and periodically polling to see if there is space for the item.

You set the upper limit by specifying it in the constructor. So if you want a BlockingCollection that can hold at most 100,000 items, you would write:

BlockingCollection<MyType> = new BlockingCollection<MyType>(100000);

To determine the bounded capacity of an existing collection, query the BoundedCapacity property. If there is no bound on the capacity, BoundedCapacity will return int.MaxValue.

Adding items to a BlockingCollection

To add an item to a BlockingCollection, you call the Add method, or one of the overloads of the TryAdd method. Add adds the item to the collection. If the collection is already at capacity, Add blocks until the item can be added.

There are several overloads of TryAdd. TryAdd(T) tries to add the item and returns immediately. The return value is True if the item was added, or False if the collection was full. TryAdd(T, time) will try to add the item within the specified time period. So TryAdd(t, 1000) will try multiple times within a one-second period, until either the item is added successfully or the interval expires. TryAdd(T, Timeout.Infinite) is equivalent to calling Add(T). It will wait indefinitely.

When you're finished adding items to the collection, you can call CompleteAdding to indicate that no more items will be added. Marking the collection as complete for adding prevents any other items from being added (attempts will result in an InvalidOperationException), and calls to remove items will not block if the collection is empty.

Removing items from a BlockingCollection

The method to remove an item from a BlockingCollection is Take. Take returns the next item from the collection. If there are no items in the collection, Take will block until an item is added. If the collection has been marked as complete for adding and there are no items, then Take throws InvalidOperationException. Calling Take probably isn't a good idea if there are multiple threads removing items from the collection, because it's impossible to prevent a situation in which Take is called to remove an item from an empty collection.

A better option is the TryTake method with an infinite timeout, which will return False if an item cannot be removed--even if the collection has been marked as complete for adding. The code below shows the difference

BlockingCollection<int> b = new BlockingCollection<int>(10);
b.CompleteAdding();
int i;
bool rslt = b.TryTake(out i, Timeout.Infinite);  // rslt will be false
i = b.Take();  // throws InvalidOperationException

There is also a TryTake method that does not wait at all. If you want to get an item if there's one ready right now, but not wait for one, you could write:

int i;
if (b.TryTake(out i))
{
    // got item
}

Of course, most programs that process things from a collection do so in a loop, continuing until there are no more items to process. BlockingCollection makes that very easy. If your processing loop just waits for the next item, processes it, and then repeats, the loop couldn't be simpler:

int i;
while (b.TryTake(out i, Timeout.Infinite))
{
    // process item
}
// Collection is empty

If you want your program to do other things when there's no item available in the collection, you have to call TryTake with a smaller timeout value, and check the IsCompleted property periodically so you know when to quit.

while (!b.IsCompleted)
{
    int i;
    if (b.TryTake(out i, TimeoutValue))
    {
        // Process item
    }
    else
    {
        // do something else in lieu of processing an item
    }
}
// Collection is empty

As I said before, instantiating a BlockingCollection without specifying a backing store will create a collection that uses a ConcurrentQueue as the backing store. Using BlockingCollection in that way gives you a very good producer/consumer FIFO queue. Producers add items to the collection by calling Add or TryAdd, and the BlockingCollection makes sure that the queue doesn't grow too large. Consumers can call TryTake to remove items, confident that the items will be removed in the order in which they were added, and not having to worry about polling or handling special cases. All the complicated blocking and polling code is handled by BlockingCollection, and synchronization to prevent data structure corruption is handled by the underlying ConcurrentQueue.

It's very likely that if you're using ConcurrentQueue in your programs, you would be better off using BlockingCollection. It will almost certainly simplify your code.