In C#, there are many classes that we can use to represent a group of objects that we may manipulate concurrently using multiple threads. One such class is the ConcurrentBag<T>
.
In this article, we will learn how to add, access, and remove elements from a ConcurrentBag
in C#. Then, we will discuss the advantages and disadvantages of this collection type.
Let’s begin.
What Is a ConcurrentBag in C#?
The ConcurrentBag<T>
is a generic collection type in the System.Collection.Generic
namespace that represents an unordered collection of objects. It implements the IProducerConsumerCollection<T>
interface.
It is a thread-safe collection that is suitable for scenarios where we want to add and remove items concurrently using multiple threads. When we use this collection type, the order in which we add items to our collection, may not be the same as when we retrieve them. Additionally, we cannot access items in a ConcurrentBag
through an indexer.
How to Create a ConcurrentBag in C#
Now, let’s look at the various ways in which we can create a ConcurrentBag in C#.
Creating an Empty ConcurrentBag in C#
When we create an object of the ConcurrentBag<T>
type, we have to state the data type of the items it would store. For example, let’s create a ConcurrentBag
that will contain integer items:
ConcurrentBag<int> myNumbers = new();
Here, we declare an empty ConcurrentBag
that we can use to store integer values.
Creating a ConcurrentBag With Initial Items
Also, we can create an instance of the ConcurrentBag<T>
class by specifying the initial values that it should contain:
var myConcurrentBag = new ConcurrentBag<int>() { 2, 4, 6, 8, 10 };
We initialize a ConcurrentBag
that doesn’t have a specified capacity. Its size can increase or decrease dynamically depending on the operations that we perform on it. Also, we can iterate through this collection using a foreach
loop.
Creating a ConcurrentBag by Passing Another Collection as an Argument
Another way we can create a ConcurrentBag<T>
object is to pass any collection that implements the IEnumerable<T>
interface to the constructor:
var myList = new List<int>() { 1, 2, 3, 4, 5, 6, 7 }; var myConcurrentBag = new ConcurrentBag<int>(myList);
It is important to note that the data type of the collection we pass in, must be the same as the type of ConcurrentBag
.
Working With a ConcurrentBag in a Multithreaded Environment
In this section, let’s dive deeper and see how we can ensure thread safety in a multithreaded environment by using the ConcurrentBag<T>
collection class. To start with, we will define a ConcurrentBag
object, and then implement methods to manipulate its values from multiple threads.
Adding Elements to a ConcurrentBag Concurrently
First, let’s implement a method that declares a ConcurrentBag
, and then concurrently adds elements to it:
public static ConcurrentBag<int> CreateAndAddToConcurrentBagConcurrently() { ConcurrentBag<int> numbersBag = new(); Parallel.For(0, 1000, i => { numbersBag.Add(i); }); return numbersBag; }
First, we declare a ConcurrentBag<int>
object. After that, we invoke the Parallel.For
method to add items to our bag. We can use this method to execute a loop in parallel, with each iteration running on a separate thread.
The loop iterates from 0 to 1000 and adds each value to our ConcurrentBag
. This allows us to add elements concurrently, without explicit synchronization (using the lock
statement).
To get the number of elements in the returned collection, we can invoke the built-in Count
property:
var myConcurrentBagCount = myConcurrentBag.Count;
Remove Elements From a ConcurrentBag
When we want to remove an item from a ConcurrentBag
, we use the TryTake
method. The TryTake
method removes an item from the bag and returns it to the caller. If the collection is empty, it returns false
:
public static ArrayList RemoveFromConcurrentBag(ConcurrentBag<int> numbersBag) { var result = new ArrayList(); if (numbersBag.TryTake(out int number)) { result.Add(number); } return result; }
In this method, we pass the bag as an argument. Of course, we want to remove an element from that bag. So, in the TryTake
method, we pass an out
parameter that will reference the returned value. Finally, we remove and return any number found in the collection.
We see that the CollectionBag<T>
type is non-deterministic. This implies the order of removing items from the collection, may not be the same as the order of adding items.
Now, let’s see what happens when we try to remove items from a ConcurrentBag
concurrently:
public static ArrayList RemoveFromConcurrentBagConcurrently(ConcurrentBag<int> bag) { var numbersList = new ArrayList(); Parallel.For(0, 20, i => { if (bag.TryTake(out int number)) { Console.WriteLine($"Thread {Environment.CurrentManagedThreadId} took item: {number}"); numbersList.Add(number); } }); return numbersList; }
Again, we use the Parallel.For
method to execute a loop concurrently, with each iteration running on a separate thread.
The loop iterates twenty times and tries to remove an item from our collection using the TryTake
method. In each iteration, when we remove an item from the ConcurrentBag
, we print it to the console along with the Id of the thread that removed it. Then, the item is added to our ArrayList
.
Finally, we populate the list with the returned values from our ConcurrentBag
.
It is important to note that the order in which we remove items from the ConcurrentBag
may not be the same as the order in which we print them on the console because of the concurrent behavior of the operation.
That said if we want to check if a ConcurrentBag
object is empty, we can use the IsEmpty
property:
var isMyConcurrentBagEmpty = myConcurrentBag.IsEmpty;
Access an Element in a ConcurrentBag
Next up, let’s see how we can access an element in a ConcurrentBag
. We can do this using the TryPeek
method. When we invoke this method, it returns an item from the collection but does not remove it:
public static ArrayList AccessItemFromAConcurrentBag(ConcurrentBag<int> bag) { var result = new ArrayList(); if (bag.TryPeek(out int number)) { result.Add(number); } return result; }
The TryPeek
method retrieves an object from the bag without modifying the content of the bag and returns it to the caller through the number
variable.
Now, if the bag contains an item and the TryPeek
method returns true
, we add the value of that item (stored in the number
variable) to our ArrayList
. Then, we return the list to the caller.
Finally, let’s see how to concurrently access an item from multiple threads, using the TryPeek
method:
public static void AccessItemFromAConcurrentBagConcurrently(ConcurrentBag<int> bag) { Parallel.For(0, 50, i => { if (bag.TryPeek(out int number)) { Console.WriteLine("Thread {0} peeked item: {1}", Environment.CurrentManagedThreadId, number); } }); }
Here, we invoke the Parallel.For
method to execute a loop concurrently, with each iteration running on a separate thread.
Finally, the loop iterates fifty times, and each iteration tries to access an object from our collection using the TryPeek
method. If the bag has an item, we print it on the console along with the ID of the thread that we use to access it.
Other Methods of the ConcurrentBag Class
Now, let’s discuss some of the most commonly used methods of the ConcurrentBag<T>
class.
ToArray
The ToArray
method copies all the elements of a ConcurrentBag
into a new array:
public static int[] ConcurrentBagToArrayMethod(ConcurrentBag<int> bag) { var myArray = new int[bag.Count]; myArray = bag.ToArray(); return myArray; }
Here, the ToArray
method populates myArray
with items from myConcurrentBag
. We should note that the order of the items in the array, may not be the same as the order in which we added them to the bag.
CopyTo
We use this method to copy the items in a ConcurrentBag
to an existing one-dimensional array, starting at a specified index:
public static int[] ConcurrentBagCopyToMethod(ConcurrentBag<int> bag) { var someArray = new int[bag.Count]; bag.CopyTo(someArray, 0); return someArray; }
Here, we populate the array with elements from the bag, starting at the index, 0. Again, the order of the items in the array, may not be the same as the order of the items when we inserted them into the bag.
Clear
This is a thread-safe method that we can invoke when we want to remove all items from a ConcurrentBag
:
public static void ConcurrentBagClearMethod(ConcurrentBag<int> bag) { bag.Clear(); Console.WriteLine($"My concurrent bag contains {bag.Count} item."); // My concurrent bag contains 0 item. }
As we can see, the bag is now empty.
Visit here, to see all the methods from the ConcurrentBag<T>
class.
When to Use a ConcurrentBag in C#
We should use a ConcurrentBag
when we have multiple threads that need to add or remove items from a collection concurrently. This ensures that our collection remains in a consistent state.
Also, we can use this collection type when we want to process grouped objects concurrently and don’t need to preserve the order of the objects.
For example, the ConcurrentBag
type is suitable for situations where we want to process a group of objects by a set of worker threads. When we use this collection class, the worker threads can concurrently retrieve items from the collection and process them. Ensuring that we do not worry about synchronization or race conditions.
That said, we must remember that items in a ConcurrentBag
are unordered. Therefore, if we need to preserve the order of items, we should consider using a different concurrent collection type, like the ConcurrentQueue
or the ConcurrentStack
.
Benefits of a ConcurrentBag in C#
When we use the ConcurrentBag
collection type, we can be sure that our application is thread-safe. This is so because the ConcurrentBag
was designed to be used concurrently by multiple threads, and it provides thread safety for adding and removing items.
Also, using the ConcurrentBag
can enhance our program’s performance because it uses a lock-free mechanism to support add and remove operations. This can result in better performance when compared with other concurrent collection types that use the lock
construct.
Finally, because of its unordered nature and ability to increase or decrease its size dynamically, the ConcurrentBag
class offers more flexibility. This allows us to store, remove and retrieve items in any order.
Drawbacks of a ConcurrentBag in C#
To start with, due to the unordered nature of the ConcurrentBag
class, we cannot use it to represent a collection of objects where order is important to us.
Also, if we use a ConcurrentBag
to store a sequence of items that need to be processed in a specific order, the unordered nature of the bag can make it difficult to ensure that the items are processed in the correct order.
Finally, when we use a ConcurrentBag
, we cannot use indexers, iterators, or other methods to access or modify the items in the bag in a specific order. This is because the ConcurrentBag<T>
type does not provide many ways to access or modify the items in the bag.
Conclusion
In this article, we have seen that the ConcurrentBag
is an efficient collection type in C# for storing and retrieving items concurrently from multiple threads. However, it does not guarantee the order in which we add and remove items from the collection. Finally, when we are to choose a collection type for concurrent programming in C#, we should consider the specific requirements of our application and select the most suitable collection.
Great overview of ConcurrentBag class! In which scenarios is better to use ConcurrentBag over Channels and vice versa?
Thanks!
Hi Branislav.
Well, I didn’t use channels but as much as I know, they are created to provide a way for communication between threads or tasks using async messaging. So the purpose is completely different from the ConcurrentBag where you want to add and remove items in a thread-safe way.
So, in general, you would use ConcurrentBag when you need a thread-safe collection for storing and accessing items concurrently, and you would use Channels when you need to communicate between threads or tasks using asynchronous message-passing.
Again, I know that my answer is a bit wide here, but as I said, I didn’t use channels so I can’t go into details about it.