Telerik blogs

Check out in this blog post how asynchronous programming works in .NET by creating an efficient breakfast.

The massive use of technology has demanded a high workload from modern systems—after all, there are scenarios where a single application needs to execute thousands of transactions in a short time. For this to be possible, some techniques must be put into practice—one of the most-used is asynchronous programming.

Asynchronous programming is a subject that many developers use but often do not know exactly how it works—after all, it is not so simple to understand.

In this article, breakfast will be used to demonstrate how asynchronous programming works in .NET and what its advantages are over synchronous programming.

First, we will see an introduction to the subject, and then we will create two console applications in .NET to see in practice how to use the .NET features for asynchronous programming.

Understanding the Concept of Asynchronous Programming

It is very likely that you, as a developer, have already come across situations where the program needed to perform a simple action such as validating a user’s data or extracting alphanumeric characters of some value.

Usually, activities of this type take little time and there is no problem that each task waits for the previous task to complete. In this case, we’re using “synchronous programming.”

However, there are scenarios where the execution of some tasks can take considerable time, such as calling an external API or a procedure in the database. So that other tasks do not have to wait all this time to be executed, the best option is to use “asynchronous programming,” where each task works independently and is orchestrated by the system core itself.

Asynchronous Programming in .NET

.NET provides support for asynchronous programming through the Task Asynchronous Programming model (TAP). This native capability lets you avoid performance bottlenecks and improve an application’s responsiveness through a streamlined approach that leverages asynchronous support in the .NET Framework 4.5 and higher, .NET Core and Windows Runtime.

TAP has all the advantages of asynchronous programming with minimal effort.

Synchronous vs. Asynchronous

To see in practice the advantages of an asynchronous application, we will compare it with the same application but done synchronously. So, as a first example, we will create a simple console app that simulates breakfast preparation in a synchronous way.

Creating the Synchronous App

Our synchronous breakfast will follow this order:

  1. Fill a cup of coffee
  2. Fry two eggs
  3. Fry three slices of bacon
  4. Fill a glass with orange juice

As we are working with synchronization, each task will only start when the previous task finishes—that is, the order must be maintained, so the process will only end when the orange juice is in the glass.

You can access the complete source code of the project at this link: source code.

To create the application, you can follow the steps below. (Note: This post was written with .NET 6, but .NET 7 is now available!)

In Visual Studio:

  • Select Create a new project
  • Select Console App
  • Name the project (SyncBreakfast is my suggestion)

Via terminal:

dotnet new console --framework net6.0 -n SyncBreakfast

Then, open the project with your favorite IDE, and creates a new folder called “Models” and inside it creates the class below:

  • Breakfast
namespace SyncBreakfast.Models;
public class Breakfast
{
    public Coffee Coffee { get; set; } = new Coffee();
    public Bacon Bacon { get; set; } = new Bacon();
    public Egg Egg { get; set; } = new Egg();
    public Juice Juice { get; set; } = new Juice();
} 

public class Coffee
{
    public Coffee PourCoffee(int cup)
    {
        Console.WriteLine($"Pouring {cup} of coffee");
        Task.Delay(1000).Wait();
        return new Coffee();
    }
}

public class Bacon
{
    public Bacon FryBacon(int slices)
    {
        for (int slice = 0; slice < slices; slice++)
        {
            Console.WriteLine("Cooking a slice of bacon");
            Task.Delay(2000).Wait();
        }
        return new Bacon();
    }
}

public class Egg
{
    public Egg FryEggs(int eggs)
    {
        for (int egg = 0; egg < eggs; egg++)
        {
            Console.WriteLine("Cooking a egg");
            Task.Delay(3000).Wait();
        }
        return new Egg();
    }
}

public class Juice
{
    public Juice PourJuice()
    {
        Console.WriteLine("Pouring orange juice");
        Task.Delay(1000).Wait();
        return new Juice();
    }
}

Note that the Breakfast class has four subclasses: Coffee, Bacon, Egg and Juice. Each of them has a method for executing its respective task that takes a certain amount of time to execute and displays a message when it is ready.

As the methods are running synchronously, each task runs after the previous task finishes. In this scenario, it’s like we have a single chef to prepare breakfast, and he needs to finish putting the coffee in the cup first to start frying the eggs. In this synchronous scenario, he can’t perform more than one task at the same time—our synchronous cook is not very efficient.

To check how this execution will be, in the Program class, replace the existing content with the code below:

using SyncBreakfast.Models;

var initialTime = DateTime.Now.Second;

var breakfast = new Breakfast();

var coffee = breakfast.Coffee.PourCoffee(1);
Console.WriteLine("Coffee is ready");

var bacon = breakfast.Bacon.FryBacon(2);
Console.WriteLine("Bacon is ready");

var egg = breakfast.Egg.FryEggs(2);
Console.WriteLine("Egg is ready");

var juice = breakfast.Juice.PourJuice();
Console.WriteLine("Juice is ready");

var finishTime = DateTime.Now.Second;

Console.WriteLine($"Breakfast is ready! The process took {finishTime - initialTime} seconds");

Here we are calling the breakfast preparation methods and the execution time of the whole process in seconds is also shown. When running the project we get the following result:

Execution Sync App

As we can see in the image above, our synchronous chef prepared breakfast one task at a time, and at the end of the process, the total execution time was 13 seconds.

Synchronous flowshart.png

But how much time would it take if breakfast were prepared asynchronously considering the same time for each task?

Next, we will implement the asynchronous mode of breakfast preparation and check if it is possible to be faster than 13 seconds.

Creating the Asynchronous App

In the same way as before, to create the asynchronous app you can follow the steps below:

In Visual Studio:

  • Select Create a new project
  • Select Console App
  • Name the project (AsyncBreakfast is my suggestion)

Via terminal:

dotnet new console --framework net6.0 -n AsyncBreakfast

Then, create a new folder called “Models” and inside it create the class below:

  • Breakfast
namespace AsyncBreakfast.Models;
public class Breakfast
{
    public Coffee Coffee { get; set; } = new Coffee();
    public Bacon Bacon { get; set; } = new Bacon();
    public Egg Egg { get; set; } = new Egg();
    public Juice Juice { get; set; } = new Juice();
}

public class Coffee
{
    public async Task<Coffee> PourCoffee(int cup)
    {
        Console.WriteLine($"Pouring {cup} of coffee");
        await Task.Delay(1000);
        return new Coffee();
    }
}

public class Bacon
{
    public async Task<Bacon> FryBacon(int slices)
    {
        for (int slice = 0; slice < slices; slice++)
        {
            Console.WriteLine("Cooking a slice of bacon");
            await Task.Delay(2000);
        }
        return new Bacon();
    }
}

public class Egg
{
    public async Task<Egg> FryEggs(int eggs)
    {
        for (int egg = 0; egg < eggs; egg++)
        {
            Console.WriteLine("Cooking a egg");
            await Task.Delay(millisecondsDelay: 3000);
        }
        return new Egg();
    }
}

public class Juice
{
    public async Task<Juice> PourJuice()
    {
        Console.WriteLine("Pouring orange juice");
        await Task.Delay(1000);
        return new Juice();
    }
}

Note that the model class in asynchronous format is very similar to the one we created earlier in the synchronous application. Nonetheless, here we are using the features available in .NET to create asynchronous methods via the keyword async and returning an object Task that represents a single operation, does not return a value and is usually performed asynchronously. You can read more about this data type here: Task Class Definition.

Now we need to create the method that will use the model class, so, replace the code in the Program.cs file with:

using AsyncBreakfast.Models;

var initialTime = DateTime.Now.Second;

await BreakfastProcess();

async Task BreakfastProcess()
{
    var breakfast = new Breakfast();
    var coffeeTask = breakfast.Coffee.PourCoffee(1);
    var baconTask = breakfast.Bacon.FryBacon(2);
    var eggTask = breakfast.Egg.FryEggs(2);
    var juiceTask = breakfast.Juice.PourJuice();

    var breakfastTasks = new List<Task> { coffeeTask, baconTask, eggTask, juiceTask };

    while (breakfastTasks.Count > 0)
    {
        Task finishedTask = await Task.WhenAny(breakfastTasks);

        if (finishedTask == coffeeTask)
        {
            Console.WriteLine("Coffee are ready");
        }
        else if (finishedTask == baconTask)
        {
            Console.WriteLine("Bacon is ready");
        }
        else if (finishedTask == eggTask)
        {
            Console.WriteLine("Egg is ready");
        }
        else if (finishedTask == juiceTask)
        {
            Console.WriteLine("Juice is ready");
        }
        breakfastTasks.Remove(finishedTask);
    }
}

var finishTime = DateTime.Now.Second;
Console.WriteLine($"Breakfast is ready! The process took {finishTime - initialTime} seconds");

In the code above, we create an asynchronous method called “BreakfastProcess” and inside it, we assign the return of each method to a variable. Then we add the returned tasks to a list. This list in turn is traversed and a message is displayed depending on the task that is currently being processed.

Note that we are using the .NET native “WhenAny” method and that it creates a task that will complete when any of the given tasks are completed. At the end of the process, the message process completion time is displayed.

Thus, unlike the previous approach, it is no longer necessary to wait for the completion of a task to start another. After all, through the asynchronous method, all will be executed simultaneously and the completion message will be displayed as the tasks are being completed.

If we run the program, we will get the following result:

Execution Async App

As can be seen in the image above, when running the app asynchronously, the tasks are independent, so they are executed without the need for the previous task to be finished. With that, the execution time fell by half—that is, the synchronous mode took 13 seconds to finish executing, while in the asynchronous mode took only 6 seconds.

Asynchronous flowshart.png

Conclusion

As shown in the article, .NET has native resources for working with asynchronous methods, so the developer doesn’t have to worry about too many details and can focus on how to use these resources in the best way.

An important point to consider is that although asynchronous methods are more efficient, one should always take into account when their use is really necessary. In simple scenarios, where there is no expressive time, perhaps the best option is the synchronous methods, but otherwise, it is valid to evaluate the use of asynchronous programming.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.