Thread Safe List in C#

Muhammad Maisam Abbas Feb 12, 2024
  1. Thread Safe List With the ConcurrentBag Class in C#
  2. Thread Safe List With the ConcurrentQueue Class in C#
  3. Thread Safe List With the SynchronizedCollection Class in C#
  4. Thread Safe List With the ConcurrentStack Class in C#
  5. Conclusion
Thread Safe List in C#

Navigating the intricate realm of concurrent programming in C# demands a robust understanding of thread safety. As multiple threads collaborate to execute tasks simultaneously, the risk of conflicts in shared data structures, such as lists, becomes a central concern.

Without proper synchronization, unpredictable behavior and data corruption may ensue. In this comprehensive exploration, we delve into five key thread-safe mechanisms.

Thread Safe List With the ConcurrentBag Class in C#

The ConcurrentBag class is used to create a thread-safe unordered collection of data in C#. The ConcurrentBag class is very similar to the List in C# and can be used as a thread-safe list in C#.

To use the ConcurrentBag class, we have to import the System.Collections.Concurrent namespace in our project. Multiple threads can access a ConcurrentBag object simultaneously, but the contents inside the ConcurrentBag object can only be modified synchronously.

It makes it both usable with multiple threads for concurrent operations and safe from accidental data loss.

See the following example.

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

class ThreadSafeListExample {
  static ConcurrentBag<int> threadSafeBag = new ConcurrentBag<int>();

  static void Main() {
    Task task1 = Task.Run(() => AddItemsToBag(1, 5));
    Task task2 = Task.Run(() => AddItemsToBag(6, 10));

    Task.WaitAll(task1, task2);

    Console.WriteLine("Thread-Safe Bag Contents:");
    foreach (var item in threadSafeBag) {
      Console.Write(item + " ");
    }
  }

  static void AddItemsToBag(int start, int end) {
    for (int i = start; i <= end; i++) {
      threadSafeBag.Add(i);
    }
  }
}

We commence by declaring a ConcurrentBag<int> named threadSafeBag. The ConcurrentBag is specifically designed to handle concurrent operations without requiring explicit locks, making it an ideal choice for scenarios involving multiple threads.

Moving to the Main method, we initiate two parallel tasks using Task.Run. These tasks represent concurrent operations that add items to our threadSafeBag.

The Task.WaitAll ensures that the program waits for both tasks to complete before proceeding to the next steps.

The real magic happens in the AddItemsToBag method. Here, we iterate through a range of integers specified by the start and end parameters.

Within the loop, each integer is added to the ConcurrentBag using the Add method. It’s crucial to note that under the hood, ConcurrentBag ensures thread safety without requiring explicit locks.

Output:

Thread-Safe Bag Contents:
10 9 8 7 6 5 4 3 2 1

Thread Safe List With the ConcurrentQueue Class in C#

The ConcurrentQueue class is used to create a thread-safe queue data structure in C#. The ConcurrentQueue works on the principle of first in, first out, just like the List in C#.

The ConcurrentQueue object can be used instead of the List object to create a thread-safe data structure.

See the following example.

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

class ThreadSafeListExample {
  static ConcurrentQueue<int> threadSafeQueue = new ConcurrentQueue<int>();

  static void Main() {
    Task task1 = Task.Run(() => EnqueueItems(1, 5));
    Task task2 = Task.Run(() => EnqueueItems(6, 10));

    Task.WaitAll(task1, task2);

    Console.WriteLine("Thread-Safe Queue Contents:");
    while (threadSafeQueue.TryDequeue(out var item)) {
      Console.Write(item + " ");
    }
  }

  static void EnqueueItems(int start, int end) {
    for (int i = start; i <= end; i++) {
      threadSafeQueue.Enqueue(i);
    }
  }
}

In the above code, we begin with the declaration of a ConcurrentQueue<int> named threadSafeQueue. This class is specifically crafted for scenarios where multiple threads need to interact with a shared queue without the need for explicit locking mechanisms.

Entering the Main method, we initiate two parallel tasks using Task.Run. These tasks simulate concurrent operations that enqueue items into our threadSafeQueue.

The Task.WaitAll ensures that the program waits for both tasks to complete before progressing.

The heart of our example lies in the EnqueueItems method. Here, we iterate through a range of integers specified by the start and end parameters.

Within the loop, each integer is enqueued into the ConcurrentQueue using the Enqueue method. Notably, ConcurrentQueue handles thread safety internally, alleviating the need for explicit locks.

As we move to the output section, we display the contents of our thread-safe queue using a while loop with TryDequeue. This method ensures that elements are dequeued safely, and the loop continues until the queue is empty.

Output:

Thread-Safe Queue Contents:
1 2 3 4 5 6 7 8 9 10

Thread Safe List With the SynchronizedCollection Class in C#

The SynchronizedCollection class is used to create a thread-safe collection of objects of some specified type in C#. The SynchronizedCollection data structure is very similar to the List data structure in C#.

Both data structures work on the principle of first in, first out. The function to add a new element in both the SynchronizedCollection and the List data structures is Add().

See the following example.

using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;

class ThreadSafeListExample {
  static Collection<int> synchronizedCollection = new Collection<int>();
  static object lockObject = new object();

  static void Main() {
    Task task1 = Task.Run(() => AddItems(1, 5));
    Task task2 = Task.Run(() => AddItems(6, 10));

    Task.WaitAll(task1, task2);

    Console.WriteLine("Thread-Safe Collection Contents:");
    foreach (var item in synchronizedCollection) {
      Console.Write(item + " ");
    }
  }

  static void AddItems(int start, int end) {
    for (int i = start; i <= end; i++) {
      lock (lockObject) {
        synchronizedCollection.Add(i);
      }
    }
  }
}

Our journey begins with the creation of a Collection<int> named synchronizedCollection. This collection will be the foundation for our thread-safe operations, and the SynchronizedCollection wrapper will ensure that access to it is synchronized across multiple threads.

On to the Main method, two parallel tasks are initiated using Task.Run. These tasks simulate concurrent operations that add items to our synchronizedCollection.

The Task.WaitAll ensures that the program waits for both tasks to complete before progressing.

Now, let’s delve into the heart of our example - the AddItems method. This method encapsulates the logic for adding a range of integers to the synchronized collection.

Inside the loop, each integer is added within a lock block, specifically locking on the lockObject. This ensures that only one thread at a time can access and modify the collection, achieving the coveted thread safety.

As we approach the output section, the contents of our thread-safe collection are displayed using a foreach loop. The synchronization mechanisms provided by SynchronizedCollection and the lock statement ensure that the iteration occurs safely, even in a multithreaded environment.

Output:

Thread-Safe Collection Contents:
1 2 3 4 5 6 7 8 9 10

Thread Safe List With the ConcurrentStack Class in C#

In C#, one powerful tool for achieving thread safety in such scenarios is the ConcurrentStack class. This class provides a thread-safe collection, ensuring seamless operations in multithreaded environments.

In this exploration, we’ll unravel the significance of thread safety and guide you through a practical example of implementing a thread-safe list using the ConcurrentStack method.

See the following example.

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

class ThreadSafeListExample {
  static ConcurrentStack<int> threadSafeStack = new ConcurrentStack<int>();

  static void Main() {
    Task task1 = Task.Run(() => PushItemsToStack(1, 5));
    Task task2 = Task.Run(() => PushItemsToStack(6, 10));

    Task.WaitAll(task1, task2);

    Console.WriteLine("Thread-Safe Stack Contents:");
    while (threadSafeStack.TryPop(out var item)) {
      Console.Write(item + " ");
    }
  }

  static void PushItemsToStack(int start, int end) {
    for (int i = start; i <= end; i++) {
      threadSafeStack.Push(i);
    }
  }
}

We start by declaring a ConcurrentStack<int> named threadSafeStack. This collection is engineered to handle concurrent push and pop operations without the need for explicit locks, making it an optimal choice for multithreaded scenarios.

Transitioning to the Main method, we initiate two parallel tasks using Task.Run. These tasks simulate concurrent operations that push items onto our threadSafeStack.

The Task.WaitAll ensures the program waits for both tasks to complete before proceeding.

The core of our example resides in the PushItemsToStack method. Inside the loop, a range of integers, specified by the start and end parameters, is pushed onto the ConcurrentStack using the Push method.

Notably, ConcurrentStack internally manages thread safety, eliminating the need for explicit locks.

As we navigate to the output section, the contents of our thread-safe stack are displayed using a while loop with TryPop. This method ensures safe popping of elements, and the loop continues until the stack is empty.

Output:

Thread-Safe Stack Contents:
10 9 8 7 6 5 4 3 2 1

Conclusion

The discussed methods—ConcurrentBag, ConcurrentQueue, SynchronizedCollection, and ConcurrentStack—serve as invaluable assets in the dynamic landscape of concurrent programming. Seamlessly integrating these mechanisms empowers developers to tackle the challenges of managing shared resources across multiple threads.

Through elegant solutions and streamlined approaches, these methods contribute to the creation of scalable, resilient, multithreaded applications. As we navigate the ever-evolving demands of parallel execution, the importance of thread safety remains pivotal, and these tools stand as reliable companions in this journey.

Muhammad Maisam Abbas avatar Muhammad Maisam Abbas avatar

Maisam is a highly skilled and motivated Data Scientist. He has over 4 years of experience with Python programming language. He loves solving complex problems and sharing his results on the internet.

LinkedIn

Related Article - Csharp Thread

Related Article - Csharp List