Using Asynchronous Actions

Discuss the benefits and implementation of asynchronous programming.

There are plenty of use cases for implementing synchronous actions within an application. At some point, those synchronous actions won’t be able to scale out should the load on the application increase. This is where asynchronous actions come in. These actions allow many executions to be made using different contexts, whereas synchronous actions will block other actions from executing until the invoked action is complete. This ability to execute without the need to wait for a response allows large-scale message sending without any concern of thread-blocking, which can lead to longer wait times and less-than-desirable performance.

The notion of asynchronous actions has been a paradigm in programming for many years. Using callback functions, which are meant to be executed when one method has completed and returned a value, has been a construct in JavaScript, as well as other languages. Specific patterns such as asynchronous JavaScript and XML (AJAX) were some of the building blocks of web applications using frameworks such as jQuery. Later on, constructs such as promises also helped to extend asynchronous method calls through inline function handlers, providing a more succinct way to define callbacks.

In languages that utilize the .NET Framework, functions can be assigned to event handlers or to delegates, which will execute when a method or process triggers them synchronously. There’s also a more structured pattern known as Task Asynchronous Programming (TAP), which leverages the Task and Task<T> objects in the system threading namespace to perform asynchronous operations.

Having covered the basics of asynchronous actions and how they can be used to bolster resiliency and throughput, let’s look at some of the additional benefits of using the asynchronous programming model.

Benefits of asynchronous programming

Primarily, asynchronous programming allows us to execute a greater sum of code than synchronous programming. It also allows us to leverage compute resources differently by only using resources while a method is executing. This avoids unnecessarily assigning compute resources and should lead to more consistent response times overall. This allows us to execute more complex tasks and leverage concurrency while not needing to worry about thread pool management.

Asynchronous methods are more efficient in how they utilize available resources. Longer-running operations can use the yield construct in C# to continue executing while other operations can be invoked as there are still resources available to them. When coupled with concepts such as parallel execution, asynchronous execution can facilitate further scaling of the application while preserving its responsiveness.

Asynchronous parallel processing

In .NET 6, a new construct was introduced known as Parallel.ForEachAsync(). Those familiar with the notion of parallel execution will likely have some knowledge of the Parallel.ForEach() method. This new asynchronous method allows us to perform that parallel execution while also enabling asynchronous execution.

Let’s take a look at an example of implementing Parallel.ForEachAsync() in a console app that has multiple consumers pointed at different topics in Kafka. In this case, we’ll be consuming three different topics using the aforementioned method, allowing all of them to run in parallel and consume events as they’re available.

We’ll start by using a top-level program to pull in configuration information from an appsettings.json file and create a consumer for each topic name listed in the configuration. The appsettings.json file is parsed by making a call to the ConfigurationBuilder object to retrieve the information. The list of topics is stored as a pipe-delimited string, and after splitting that string, an initial loop is executed to set up each consumer and add it to a list of consumer objects. The code for setting up each consumer is as follows:

Get hands-on with 1200+ tech skills courses.