DEV Community

Cover image for How YOU can make your .NET programs more responsive with async/await in .NET Core, C# and VS Code
Chris Noring for .NET

Posted on • Edited on • Originally published at softchris.github.io

How YOU can make your .NET programs more responsive with async/await in .NET Core, C# and VS Code

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

When we run synchronous code we block the main Thread from doing anything else than just what's it's doing currently. This makes your software and user experience slower than it needs to be.

TLDR; we have the concept of Threads in .NET/.NET Core and they are an excellent way to schedule work to be carried out in parallel. However, they might be cumbersome to use. There is, however, a library called TPL, Task Parallel Library that lives on top of the Thread model and makes it really easy to schedule and manage work.

References

WHAT

So we mentioned TPL as a library. What do we need to know? TPL is such a central and important concept that it lives in the core APIs. It's part of the System.Threading and System.Threading.Tasks namespaces. It does a lot for us like:

  • Partitioning of the work
  • Scheduling of threads on the ThreadPool
  • Cancellation support
  • State management

and other low-level details.

There are some basic concepts that we need to understand.

  • Task, a task represent an asynchronous operation, like fetching content from a file or doing a calculation that takes time. There are some interesting properties on a Task that allows us to communicate to a UI, for example, how the asynchronous work is doing, like:
    • Status, this can tell us if it's currently working on something, is done, errored out or it was canceled
    • IsCanceled, if canceled this would be set to true
    • IsFaulted, if something went wrong, like an exception, this would be set to true
    • IsCompleted, once it has finished its operation it would be set to true
  • Async/Await. The await keyword means that we wait for the asynchronous operation to end and by the end of the operation we are given the result, e.g var fileContent = await GetFileAsync(). Any method that uses the await concept would need to have async keyword as part of the method header.
  • Blocking/Non-blocking. When we use Tasks we are not blocking and other Threads can carry out work. There are exceptions though when we use the method Wait() on a Task. Then we are forcing the code to run synchronously. We will show that in our demo in the next section.

WHY

A lot of things like opening up large files or carrying out a Web Request or maybe searching through your computer - are things that can be done in parallel. This means you can return back to the user much faster with a result and your app will be perceived as faster and more responsive. Web Development already uses the concept of Tasks heavily, which is a central concept in TPL. Learning how to use TPL can really make your applications more responsive. My hope is that you with this article feel more empowered to use TPL and Tasks.

DEMO

In our Demo we will demonstrate the following:

  • Authoring methods, How to author methods using async/await and how to return different types
  • Control flow, we will show how to wait for all as well as specific Tasks
  • Blocking code, we will show how the usage of Result as well as Wait() affects your code

Scaffold a project

Let's start by creating a solution like so:

mkdir tasks
cd tasks

dotnet new sln

This should create a solution file.

Next, we will create a console project like so:

dotnet new solution -o task-demo

and now add it to the solution like so:

dotnet sln add task-demo/task-demo.csproj

Ok, we are ready to start coding. Open up an IDE, I'm gonna go with VS Code.

Authoring methods

Let's open up the file Program.cs and add the following method inside of the class Program:

static async Task<int> Sum(int a, int b) 
{
    var result = await Task.FromResult(a + b);
    return result;
}

There are some interesting things that go on above:

  • Return type, Task<int>. This tells us that it will be a Task that once resolved will return something of type int.
  • Task.FromResult(), This creates a Task given a value. We give it the calculation to perform, e.g a+b.
  • Async/Await, We can see how we use the async keyword inside of the method to wait for the result to arrive back to us. This needs to be followed by the async keyword to ensure the compiler is happy.

It's easy to think that the above method above doesn't need to be asynchronous but imagine instead that this is a calculation that takes time, then it would make more sense.

One other thing, Task.FromResult is used when the answer is immediately known so it's status is RanToCompletion and the answer is available right away so you can argue that await is unnecessary, it's already available on the Task.Result property. Another way to do the above is:

static async Task<int> Sum2(int a, int ab) 
{
  var result = await Task.Run(() => {
    // do some time-consuming work
    return a + b;
  })
  return result;
}

await Sum2(1,2)

Control flow

There's more to Tasks than just marking them async. We can ensure to wait for all or some of the tasks to finish before carrying on with our code. We have some constructs that help us control this flow:

  • Task.WaitAll(), this one takes a list of Tasks in. What you are essentially saying is that all tasks need to finish before we can carry on, it's blocking. You can see that by it returning void A typical use-case is to wait for all Web Requests to finish cause we want to return a result that consists of us stitching all their data together
  • Task.WaitAny(), we give it a list of Tasks here as well but the meaning is different. We say that as long as any of the Task has finished we are good. This usually a race for data towards an endpoint or search for a file/file content on a disk. We don't care who finished first, as long as we get a response. This is also blocking and waiting for one of the Tasks to finish
  • Task.WhenAll(), this gives you a Task back that you can interact with. When all of the tasks have finished it will resolve.
  • Task.WhenAny(), this gives you a Task back that you can interact with. When one of the Tasks has finished then it will resolve.

Let's create a demo of a Control flow. We will fake carrying out time-consuming work by adding an additional method to our class, like so:

static async Task DoSomething()
{
  await Task.Delay(2000);
}

Demo - Control flow

Now we can add some control flow code in our Main() method like so:

var start  = DateTime.Now;

var taskSum = Sum(2,2);
var taskDelay = DoSomething();
Task.WaitAll(taskSum, taskDelay);

end = DateTime.Now;

Console.WriteLine("Time taken {0}",end - start);

Our full code in Program.cs should now look like this:

using System;
using System.Threading.Tasks;
using System.IO;

namespace task_demo
{
    class Program
    {
        static async Task DoSomething()
        {
            await Task.Delay(2000);
        }
        static async Task<int> Sum(int a, int b) 
        {
            var result = await Task.FromResult(a + b);
            return result;
        }

        static void Main(string[] args)
        {
            var start  = DateTime.Now;

            var taskSum = Sum(2,2);
            var taskDelay = DoSomething();

            Task.WaitAll(taskSum, taskDelay);

            end = DateTime.Now;

            Console.WriteLine("Time taken! {0}", end-start);
        }
    }
}

Let' compile:

dotnet build

and run it:

dotnet run

We should get the following response:

4
Time taken! 00:00:02.0026920

Even though the calculation from calling Sum() took a few milliseconds, we don't get any response until 2 seconds later, when DoSomething() has finished.

If we shift our code now from WaitAll to WhenAll we would get very different behavior. The code would have kept going and reported this instead:

4
Time taken! 00:00:00.0235860

So the lesson here is that if we want the code to wait at a specific point, using WaitAny is a good idea but if you want to start up a lot of asynchronous work then use When....

We can still make the code behave correctly with WhenAll but we would need to investigate the status like so:

var twoTasks = Task.WhenAll(taskSum, taskDelay);
if(twoTasks.IsCompleted) 
{
    var end = DateTime.Now;
    Console.WriteLine("{0}", taskSum.Result);
}

DEMO - Wait any

To test this one out we create three new methods that mock opening up files. Each of the three methods has a delay built in that differs:

static async Task<string> ReadFile1() 
{
    await Task.Delay(3000);
    return "file1";
}

static async Task<string> ReadFile2()
{
    await Task.Delay(4000);
    return "file2";
}

static async Task<string> ReadFile3()
{
    await Task.Delay(2000);
    return "file3";
}

Let's update our Program() method with some code as well:

var task1 = ReadFile1();
var task2 = ReadFile2();
var task3 = ReadFile3();

start = DateTime.Now;
Task.WaitAny(task1, task2, task3);


Console.WriteLine("Task1, completed: {0}", task1.IsCompleted);

Console.WriteLine("Task2, completed: {0}", task2.IsCompleted);

Console.WriteLine("Task3, completed: {0}", task3.IsCompleted);
Console.WriteLine("Task3, completed: {0}", task3.Result);

end = DateTime.Now;
Console.WriteLine("Time taken! {0}", end - start);

As you can see above, we are waiting for one of the three tasks to finish, with this construct:

Task.WaitAny(task1, task2, task3);

Given what we know of the methods being called, ReadFile3() should finish first, after 2 seconds, but let's test that by running our program:

Task1, completed: False
Task2, completed: False
Task3, completed: True
Task3, completed: file3
Time taken! 00:00:02.0031370

We can see above that Task3 is completed and the other tasks haven't completed yet.

Using Async APIs

Ok, we now understand more about async and is able to leverage that on existing APIs. Let's look at reading the content of a file. Normally you would create a method like so:

 static async string ReadTxtFile() 
{
    using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
    {
        return sr.ReadToEnd();
    }
}

The above would block though and you wouldn't be able to do much else while this finishes. Imagine this is a really large file then it would be really noticeable. If we rewrite the method to use an async version we would instead get code looking like this:

 static async Task<string> ReadTxtFile() 
{
    using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
    {
        return await sr.ReadToEndAsync();
    }
}

This doesn't block and everyone is happy.

 Blocking code

One of the tricky parts of using TPL is knowing what calls block. You are all happy that your code is now asynchronous but suddenly you end up blocking anyway. So what shall we look out for? Well, we touched upon this subject already:

  • WaitAll and WaitAny blocks, the rule of thumb here seems to be that they return void and use the word Wait.... Sometimes you want it to wait though, so learn to be intentional with block/non-block
  • task.Result, this also blocks and waits for the result to be available
  • Wait(), this method on a Task will block and cause you to wait here until the code has finished, for example Task.Delay(2000).Wait()

Full code

This is the full code I was playing around with if you want to explore for yourself:

using System;
using System.Threading.Tasks;
using System.IO;

namespace task_demo
{
  class Program
  {
      static async Task<string> ReadTxtFile() 
      {
          using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
          {
              return await sr.ReadToEndAsync();
          }
      }

      static string ReadFileSync1() 
      {
          Task.Delay(2000).Wait();
          return "content1";
      }

      static string ReadFileSync2()
      {
          Task.Delay(2000).Wait();
          return "content2";
      }

      static string ReadFileSync3()
          {
          Task.Delay(2000).Wait();
          return "content3";
      }

      static async Task DoSomething()
      {
          await Task.Delay(2000);
      }
      static async Task<int> Sum(int a, int b) 
      {
          var result = await Task.FromResult(a + b);
          return result;
      }

      static async Task<string> ReadFile1() 
      {
          await Task.Delay(3000);
          return "file1";
      }

      static async Task<string> ReadFile2()
      {
          await Task.Delay(4000);
          return "file2";
      }

      static async Task<string> ReadFile3()
      {
          await Task.Delay(2000);
          return "file3";
      }

      static void Main(string[] args)
      {
          var start = DateTime.Now;
          var c1 = ReadFileSync1();
          var c2 = ReadFileSync2();
          var c3 = ReadFileSync3();
          var end = DateTime.Now;
          Console.WriteLine("Time taken {0}", end-start);

          start  = DateTime.Now;

          var taskSum = Sum(2,2);
          var taskDelay = DoSomething();

          Task.WaitAll(taskSum, taskDelay);

          end = DateTime.Now;

          Console.WriteLine("{0}",taskSum.Result);

          Console.WriteLine("Time taken! {0}", end-start);

          var task1 = ReadFile1();
          var task2 = ReadFile2();
          var task3 = ReadFile3();

          start = DateTime.Now;
          Task.WaitAny(task1, task2, task3);


          Console.WriteLine("Task1, completed: {0}", task1.IsCompleted);

          // this forces everyone to wait for this Task1
          // Console.WriteLine("Task1, completed: {0}", task1.Result);

          Console.WriteLine("Task2, completed: {0}", task2.IsCompleted);

          Console.WriteLine("Task3, completed: {0}", task3.IsCompleted);
          Console.WriteLine("Task3, completed: {0}", task3.Result);

          end = DateTime.Now;
          Console.WriteLine("Time taken! {0}", end - start);

      }
  }
}

Summary

In summary, we learned about the concept of Tasks and their anatomy. Additionally, we learned about Control Flows and we also discussed blocking/non-blocking code. There is more to learn though like how to cancel Tasks. Im gonna save that one for a separate article. I will add a link to Cancellation in the References section of this article.

Top comments (19)

Collapse
 
tyrrrz profile image
Oleksii Holub • Edited

await Task.FromResult that's a good one 😂
You're confusing Task.Run with Task.Result. The latter doesn't do any calculations, it just returns a completed task that you don't need to await. In fact, awaiting it just creates an unnecessary state machine that can hurt throughput.

Collapse
 
softchris profile image
Chris Noring

Well the Task result is immediately known, i.e 1+2, which is what I 'm using it for docs.microsoft.com/en-us/dotnet/ap.... Is it the best use of it? Probably not. Could I have been using Task.Run? Probably. The whole point of that method is generally to showcase the async/await combination. I think you are reading too much into a first sample method Alexey. I was making an analogy to Promise.resolve in the JavaScript world...

Collapse
 
tyrrrz profile image
Oleksii Holub

The point here was that using await with Task.FromResult is unnecessary and counterproductive.

Thread Thread
 
softchris profile image
Chris Noring

That I agree with.. I was showcasing async/await .. I will update the text to talk about the difference of Task.Run and Task.FromResult

Collapse
 
slavius profile image
Slavius

TPL and async/await have nothing in common. I can write code like:

// Calculate prime numbers using a simple (unoptimized) algorithm.

IEnumerable<int> numbers = Enumerable.Range (3, 100000-3);

var parallelQuery = 
  from n in numbers.AsParallel()
  where Enumerable.Range (2, (int) Math.Sqrt (n)).All (i => n % i > 0)
  select n;

int[] primes = parallelQuery.ToArray();
);

and converting it to async/await will not make it any faster if not the opposite.

Async/await has its specialized use cases when calling a method actually needs to spawn another thread (be it a networking or filesystem thread) that pre-empting your own thread within the operating system actually allows for other threads to run.
Please do not make async/await a golden hammer when it is clearly not.

Collapse
 
softchris profile image
Chris Noring

I appreciate your feedback. Yes, the article is about making programs be more responsive in those specific use cases, i.e File system operations, Web Request.. My examples should have been about that. I also wanted to show how Tasks and async/await worked together, which is why I opted for simpler tasks that didn't do those things. But you are right, it's not a golden hammer, but used correctly can greatly improve your program

Collapse
 
slavius profile image
Slavius

I didn't want to be rude but IMHO each article should start with a preface where there is mentioned for what it is discussed technology/framework best suited and where it is not.
I appreciate you bringing this topic into focus as it is important. It is rare to see a server with single core nowdays, and parallel programming should be understood by most programmers.

Collapse
 
bellonedavide profile image
Davide Bellone

Cool! Thanks for sharing :)
Just a tip for who is approaching async programming for the first time: in general it's not a good idea to use .Result since it wraps exceptions into an inner field.
code4it.dev/blog/asynchronous-prog...

Honestly, I haven't tried what happens if an exception occurs while using Task.WaitAny() :)

Collapse
 
siamcr7 profile image
Jamil Siam

Just to clarify, "Task.WaitAll(...)" is essentially same as "await Task.WhenAll(...)". Please correct me if I am missing something.

If I am correct in my above statement, then shouldn't latter be the best approach to wait for all the tasks to complete?

Collapse
 
softchris profile image
Chris Noring

WaitAll and WhenAll is very different. WaitAll blocks the code on that line. WhenAll returns directly with a Task.. As for await Task.WhenAll(), yes you can do that... As for one approach is better than another, not sure tbh.. I do know that when I get a task back I have the capability check it's state, cancel it and so on.. With that said, I suppose WhenAll gives us more fine-grained control to affect all three Tasks like cancel all, cancel one etc... With WaitAll I'm stuck at that line so it might be tricky to do anything but just wait. So if we ar talking about more fine-grained control I lean on agreeing with you.

Collapse
 
carlillo profile image
Carlos Caballero

Thanks Chris for this post!

Collapse
 
softchris profile image
Chris Noring

Thanks Carlos :)

Collapse
 
deanagan profile image
Dean

Hi Chris, awesome article! I have a question. When either WhenAny or WhenAll returns with a task, are the tasks already executing at that point and could finish before interacting with it?

Collapse
 
softchris profile image
Chris Noring

I would say yes. They are already running.. You can check that on task.Status for a specific Task or var taskWhenAll = Task.WhenAll(t1,t2,t3) and taskWhenAll.Status

Collapse
 
amul047 profile image
amul047 • Edited

Are you meant to say WaitAll to WaitAny in this section? thepracticaldev.s3.amazonaws.com/i...

Collapse
 
softchris profile image
Chris Noring

hi... No I mean WhenAll. So WhenAll returns right away, it returns a Task, which means it carries out the next line of code which is to calculate time taken. WaitAll blocks and this why we get a higher time taken.. (We wait until the slowest task in WaitAll have finished until we continue with the next line of code)

Collapse
 
amul047 profile image
amul047

Awesome, thanks for the clarification

Collapse
 
immersivegamer profile image
Henry Nitz

Any thoughts on continuations with tasks? I haven't yet had a need to use them, but it seems like a easy way to ensure behaviour executes after a task is completed.

Collapse
 
softchris profile image
Chris Noring

they are good.. definitely use them. I am linking to them in the references section. I just thought the article was long enough.. :)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.