C# Asynchronous and Parallel Programming Best Practices

I’ve already written about Asynchronous and Parallel programming before here and here. This is one of those subjects that can be tricky and is the source of a lot of bugs. But I feel the posts that I’ve written before might be too long or they are too specialized, hence not very approchable.

In this post I’m going to try to summarize of all those post about Asynchronous and Parallel programming. The goal is to provide a good enough but shorter advice that can be reviewed very quickly. I hope can I achieve this goal in this post.

Asynchronous Programming Best Practices Summary

  1. Understand the difference between concurrency and parallelism – concurrency refers to the ability of multiple tasks to make progress in overlapping time periods, while parallelism refers to the ability of multiple tasks to run simultaneously on different processors or cores.
  2. Be aware of deadlocks and race conditions – deadlocks occur when two or more threads are waiting for each other to release a resource, while race conditions occur when two or more threads access shared resources simultaneously.
  3. Use synchronization primitives such as locks, semaphores, and mutexes to protect shared resources – these primitives ensure that only one thread can access a shared resource at a time.
  4. Use thread-safe collections such as ConcurrentDictionary and ConcurrentQueue – these collections provide thread-safe access to shared data structures.
  5. Avoid mixing synchronous and asynchronous code, as it can lead to deadlocks and reduced performance.
  6. Use async/await for I/O-bound operations that allow the thread to be released during I/O operations.
  7. Use ConfigureAwait(false) to improve performance and avoid deadlocks in non-UI contexts.
  8. Avoid blocking on async code, as it can lead to deadlocks and reduced performance.
  9. Use Task.Run for CPU-bound long running operations that can be executed on a separate thread. Do not use Task.Run if it’s not necessary (More here)
  10. Use async Task instead of void for methods that return a Task, to allow for better error handling and debugging.
  11. Use the async/await keywords instead of the Task.Wait and Task.Result methods.
  12. Use the .ConfigureAwait(false) method to improve performance and avoid deadlocks in UI contexts.
  13. Avoid using the Task.Result property on a UI thread, as it can lead to deadlocks.
  14. Use the Task.WhenAll method to wait for multiple tasks to complete.
  15. Use the Task.WhenAny method to wait for the first task to complete.
  16. Avoid using async void methods, as they cannot be awaited and can lead to unhandled exceptions.
  17. Use the async Task<T> instead of the Task<T> for methods that return a value.
  18. Use the await using statement to ensure that IDisposable resources are properly disposed of.
  19. Use the ValueTask struct for methods that return a result that is readily available.
  20. Use the ConfigureAwait method to specify the synchronization context.

Asynchronous Programming Best Practices Examples:

  1. Use async and await to perform I/O-bound operations asynchronously.

    Example:
  2. Use ConfigureAwait(false) to improve performance when you don’t need to resume on the original context.

    Example:
  3. Use the CancellationToken.ThrowIfCancellationRequested method to check for cancellation and throw an exception if cancellation has been requested.

    Example:
  4. Use the Task.WhenAll method to execute multiple tasks concurrently.

    Example :
  5. Use the Task.WhenAny method to await the first task to complete.

    Example :
  6. Use the CancellationToken.Register method to register a delegate to be called when the token is cancelled.

    Example:
  7. Use the Task.Delay method to introduce a delay in an asynchronous method. Use this instead of Thread.Sleep

    Example :
  8. Use the async void keyword only for event handlers, and avoid using it in other methods.

    Example :
  9. Use the await Task.WhenAll method to wait for multiple tasks to complete.

    Example :
  10. Use the CancellationTokenSource.CancelAfter method to automatically cancel a task after a specified time period.

    Example :

Parallel Programming Best Practices Summary

  1. Avoid overusing parallel programming, as it can lead to reduced performance and increased complexity.
  2. Use the Task Parallel Library (TPL) for parallel programming tasks.
  3. Avoid using thread pools, use TPL instead. Thread pools can lead to deadlocks, and TPL can handle thread scheduling more efficiently.
  4. Use the Parallel class to create parallel loops. This will automatically manage threads for you.
  5. Be aware of the overhead of creating and managing threads. Don’t create more threads than necessary.
  6. Be aware of data dependencies between tasks. If one task depends on the output of another task, make sure it’s completed before starting the dependent task (You can also use TPL Dataflow)
  7. Use the lock keyword to prevent race conditions when multiple threads are accessing the same resource.
  8. Use the SpinLock class. The SpinLock class allows you to protect shared data from concurrent access.
  9. Use the SemaphoreSlim class. The SemaphoreSlim class allows you to limit the number of concurrent accesses to a shared resource.
  10. Use the ReaderWriterLockSlim class. The ReaderWriterLockSlim class allows you to protect shared data from concurrent access, while allowing multiple readers to access the data at the same time.
  11. Use the Barrier class. The Barrier class allows you to synchronize the execution of multiple threads.
  12. Avoid using shared state between tasks. Use immutable data structures or thread-safe collections.
    Use the ConcurrentBag class. The ConcurrentBag class is a thread-safe collection that allows you to add and remove items concurrently.Use the ConcurrentDictionary class. The ConcurrentDictionary class is a thread-safe dictionary that allows you to add, remove, and get items concurrently.Use the ConcurrentQueue class. The ConcurrentQueue class is a thread-safe queue that allows you to add and remove items concurrently.Use the ConcurrentStack class. The ConcurrentStack class is a thread-safe stack that allows you to add and remove items concurrently.Use the ConcurrentSkipListSet class. The ConcurrentSkipListSet class is a thread-safe set that allows you to add, remove, and check for the existence of items concurrently.
  13. Avoid using Thread.Sleep() in parallel code. Use CancellationToken.WaitHandle.WaitOne() instead.
  14. Use the Parallel.Invoke() method to execute multiple methods in parallel.
  15. Use the BlockingCollection class for thread-safe queues.
  16. Use the Parallel.ForEach method. The Parallel.ForEach method allows you to iterate over a collection of items in parallel.
  17. Use the Parallel.For method. The Parallel.For method allows you to iterate over a range of numbers in parallel.
  18. Use the Parallel.Reduce method. The Parallel.Reduce method allows you to combine the results of multiple tasks into a single result.
  19. Use the Parallel.AsParallel method. The Parallel.AsParallel method allows you to convert a sequential operation into a parallel operation.
  20. Use the ParallelOptions class. The ParallelOptions class allows you to configure the behavior of parallel operations.

Parallel Programming Best Practices Examples:

  1. Use PLINQ to parallelize LINQ queries, if they can benefit from it

    Example :
  2. Avoid using Thread.Abort to forcefully terminate parallel tasks. Instead, use CancellationToken to request cancellation and allow tasks to clean up gracefully

    Example :
  3. Use the Parallel class to create parallel loops:

    Example :
  4. Use the lock keyword/SpinLock /SemaphoreSlim/ReaderWriterLockSlim   to prevent race conditions:

    Example :
    Example :
    Example :
    Example :
  5. Avoid using shared state between tasks: Use immutable data structures or thread-safe collections to avoid race conditions. Here are some examples:

    Example :

What is CancellationTokenSource and how to use it?

The CancellationTokenSource is a class in .NET framework that is used to create a cancellation token and notify other threads when cancellation has been requested. The main purpose of CancellationTokenSource is to provide a way to cancel long-running operations, such as background tasks or asynchronous operations, in a controlled manner.

In other words, the CancellationTokenSource provides a mechanism for gracefully stopping a task or operation that is in progress. It allows you to signal to the task that it should stop processing and clean up any resources it is using.

When a CancellationTokenSource is canceled, it sets its IsCancellationRequested property to true. You can pass a CancellationToken object that is associated with a CancellationTokenSource to a long-running operation, such as a Task, and check its IsCancellationRequested property periodically to determine whether the operation should be canceled.

Overall, the CancellationTokenSource class is a valuable tool for managing long-running operations and ensuring that they can be stopped gracefully when necessary.

Here’s an example  of CancellationTokenSource usage:

Asynchronous Code and SynchronizationContext

When you work with asynchronous code, considering the environment where it runs is crucial. Asynchronous code allows other operations to continue during the execution of a specific task. However, if you overlook the context of the asynchronous code, unexpected behavior, such as errors or unintended results, might occur.

For example, an asynchronous operation running in an interruptible or cancellable context might not complete as expected. Similarly, the operation could fail or face delays if the necessary resources aren’t available in the current context.

Therefore, when writing or using asynchronous code, understanding the context it operates in is vital. You need to design your code to handle potential issues or interruptions. This includes considering resource availability, interruption potential, and the overall structure of the application or system hosting the code.

This is where the SynchronizationContext class becomes essential. It allows developers to control the context for executing asynchronous operations. Typically, asynchronous code runs on a different thread or process from the main application. But, the SynchronizationContext class ensures that you can execute these operations in specific contexts, like the UI thread. This is particularly useful for updating the user interface during long-running tasks.

The SynchronizationContext class manages the context for asynchronous operations and offers methods to schedule work on a particular thread or process. For instance, you can use its Post() method to schedule work on a specific thread or process. The Send() method allows synchronous execution on the same thread or process as the calling code.

This class is incredibly helpful in scenarios where you need to update the user interface during lengthy operations, such as downloading large files or performing complex calculations. By using the SynchronizationContext class, developers can keep the user interface responsive and updated, even during these operations.

For more information and clearer explanations, [link].

Summary

In summary, when it comes to asynchronous programming best practices, it’s crucial to understand the distinctions between concurrency and parallelism, avoid deadlocks and race conditions, use synchronization primitives and thread-safe collections, and carefully manage asynchronous code. Examples include using async/await for I/O-bound operations, CancellationToken for cancellation handling, and Task.WhenAll/Task.WhenAny for managing multiple tasks.

In parallel programming, consider using the Task Parallel Library (TPL) and avoiding thread pools, manage data dependencies, use thread-safe constructs like locks and Concurrent collections, and refrain from using Thread.Sleep. Utilizing classes like SpinLock, SemaphoreSlim, ReaderWriterLockSlim, and Barrier can help enhance parallelism and concurrency while avoiding performance bottlenecks and complexity.

Resources and Further Readings:

Asynchronous programming – C# | Microsoft Learn

Async/Await – Best Practices in Asynchronous Programming | Microsoft Learn

What is SynchronizationContext

Top 7 Common Async Mistakes (hamidmosalla.com)

Stephen Cleary’s Blog

Share...
 

Hamid Mosalla

Hi, I'm Hamid ("Arman"). I'm a software developer with 8+ years of experience in C#, .NET Core, Software Architecture and Web Development. I enjoy creating dev tools, contributing to open-source projects, and sharing insights on my blog. Outside of tech, I’m into indie cinema, classical music and abstract art.

 

Leave a Reply

Your email address will not be published. Required fields are marked *