Asynchronous Programming


Creating the Asynchronous App

Via terminal:

dotnet new console --framework net9.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:

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:

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.

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

dotnet new console --framework net9.0 -n AsyncBreakfast
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.

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:

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.

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.

Comments

Popular posts from this blog

Deferred Execution in .NET:

ICollection vs IQueryable vs IEnumerable vs IList vs List vs HashSet in C#

Why Async/Await Don't Work With Array Methods in .NET