1

I am new to the async / await feature in C# and, after some study, I think I have understood pretty well what these keywords want to accomplish. But it comes to my mind this question: There are things that async / await make it possible that cannot be done using the Task type? Consider the follow example:

static async Task<int> jobAsync()
{
    // I could be writing here "await someTask()"
    await Task.Delay(2000);
    return 1;
}

static async void simpleAsync()
{
    int i = await jobAsync();
    Console.WriteLine("Async done. Result: " + i.ToString());
}

static void simpleTask()
{
    var t = Task.Run(() => { 
    //I could be writing here "return someTask();"
    Thread.Sleep(2000); return 1; });
    t.ContinueWith(tsk => { Console.WriteLine("Task done. Result: " + tsk.Result); });
}

Now the two function "simpleTask()" and "simpleAsync()" gives the same result, for example if called into the Main method:

static void Main(string[] args)
{
    simpleTask();
    //simpleAsync();
    Console.WriteLine("Doing other things...");
    Console.ReadLine();
}

Surely this is just a simple example, but what makes the async / await really useful? In what circumstances? Thank you.

Stefan Becker
  • 5,695
  • 9
  • 20
  • 30
Joseph
  • 43
  • 9
  • 2
    There are some optimizations for awaiters (well, mostly on .net core, not so much on .net framework), but this is really mostly about readability. async/await is *much* easier to read than a chain of `ContinueWith` – Kevin Gosse Aug 20 '19 at 21:35
  • 2
    One big difference is that `simpleTask` creates a thread that immediately blocks while `simpleAsync` does not create a thread and does not block. Threads are expensive limited resources, you should not create threads that block. – Dour High Arch Aug 20 '19 at 21:38
  • 2
    Possible duplicate of [Difference between await and ContinueWith](https://stackoverflow.com/questions/18965200/difference-between-await-and-continuewith) – Lorentz Vedeler Aug 20 '19 at 21:41
  • 1
    @DourHighArch That's because the example was poorly chosen. The example should have been `JobAsync().ContinueWith(tsk => { Console.WriteLine("Task done. Result: " + tsk.Result); })` – Kevin Gosse Aug 20 '19 at 21:41
  • 3
    You can place an `await` anywhere, such as in any of the `for` statements, a `using` statement, etc. It's technically possible to write equivalent code without using it, but the result would be quite unreadable, littered with `.ContinueWith()` throughout your control flow. – Patrick Roberts Aug 20 '19 at 21:47
  • Other than readability, another difference could be that awaiters aren't necessarily coupled to tasks. You can write custom awaiters as you see fit. For instance on WinRT, there are awaiters for `IAsyncOperation`: https://learn.microsoft.com/en-us/uwp/api/windows.foundation.iasyncoperation_tresult_ – Kevin Gosse Aug 20 '19 at 22:06

3 Answers3

5

What c# async / await can do that a Task type can't?

Asynchronous code has existed for a really, really long time. Probably the 1960s, if I had to guess. In the .NET timeframe, there have been asynchronous code patterns from the very beginning - in .NET 1.0.

So, one (somewhat pedantic) answer to the question above is "nothing". async and await bring no new capabilities; asynchronous code has always been possible.

what makes the async / await really useful? In what circumstances?

The primary benefit of async and await is code maintainability. I have a talk that I give that explores the evolution of asynchronous design, from events to callbacks to promises and finally async/await. At each step, the code is easier to maintain, but it never approaches equivalent maintainability as synchronous code until async and await come into the picture.

Specifically, your simpleTask is using promises with continuations; this is the next-most-maintainable pattern. It's similar to async/await until you try to build something with state. Try doing this with promises, and you'll see what I mean:

Task<int> GetDataCoreAsync();

async Task<int> GetDataAsync()
{
  int retries = 10;
  while (retries > 0)
  {
    try { return await GetDataCoreAsync(); }
    catch (Exception ex)
    {
      --retries;
      if (retries == 0)
        throw;
    }
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
2

The key thing to know about the difference between async and tasks revolves around what we call the "message pump". The way multithreading works in modern .NET applications, there is a big loop. The loop descends into your code and executes everything. When control is returned to the message pump, other things can have their turn.

If anyone remembers the old WinForms days, there was a problem with UI updates. Sometimes the UI would completely freeze up when the program was in a loop. The solution was to add Application.DoEvents(); into the loop. Not many people actually understood what this did. This told the message pump to take a break and check for other code that might be waiting to run. This included the code that got called on mouse clicks, thus magically unfreezing the UI. async is a more modern approach to the same concept. Any time the program execution arrives at an await, it runs the code however it deems appropriate without blocking the pump.

Tasks, on the other hand, always (with a few rare exceptions) spin up a new thread to handle the blocking problem. This works, but async may come up with a better (more optimised) solution. Tasks have the advantage of being able to run multiple pieces of synchronous code in parallel which allows execution to continue in the same function before it finishes.

This begs the question: When would we use async in a task as demonstrated below?

Task.Run(async () => await MyFuncAsync());

Any thoughts in the comments would be appreciated.

  • You raise a pretty good point, in that `await` can post the continuation to the current SynchronizationContext, while `ContinueWith` isn't aware of it (though that can be worked around using `TaskScheduler.FromCurrentSynchronizationContext`) – Kevin Gosse Aug 20 '19 at 21:59
  • Though this is not actually about `await` but specifically about `TaskAwaiter`. Other awaiters might not be aware of the synchronization context. – Kevin Gosse Aug 20 '19 at 22:01
  • You'd use `async`/`await` in that `Task.Run` call so that if `MyFuncAsync()` threw an exception you'd see this line of code in your stack trace. – StriplingWarrior Aug 20 '19 at 22:12
-1

There are a ton of really detailed points we could dive into, but a simple and glaring one from the example you just gave is that your Main method is forced to wait for the user to input a line in order to safely end the program.

If you did this, you could allow the program to end automatically after all the asynchronous work was done:

static async Task Main(string[] args)
{
    var task = simpleAsync();
    Console.WriteLine("Doing other things...");
    await task;
}

static async Task simpleAsync()
{
    int i = await jobAsync();
    Console.WriteLine("Async done. Result: " + i.ToString());
}

You'll notice that in order to make that work, we're actually leveraging the Task class in addition to async/await. That's an important point: async/await leverages the Task type. It's not an either/or proposition.

In detail, it's important to recognize that the simpleTask example you provided is not about "using the Task type", but rather "using Task.Run()". Task.Run() executes the given code on a completely different thread, which has ramifications when you're writing things like web applications and GUI-based applications. And, as Kevin Gosse pointed out in a comment below, you could call .Wait() on a Task that gets returned in order to block your main thread until the background work is done. This would likewise have some serious implications if your code were running on a UI thread in a GUI-based application.

Using Tasks as they're generally meant to be used (usually avoiding Task.Run(), .Wait(), and .Result) gives you good asynchronous behavior with less overhead than spinning up a bunch of unnecessary threads.

If you're willing to accept that point, then a better comparison of async/await versus not using it would be one that leverages methods like Task.Delay() and ContinueWith(), returning Tasks from each of your methods to stay asynchronous, but simply never uses the async keyword.

In general, most of the kinds of behavior you could get from using async/await can be replicated by programming to Tasks directly. The biggest difference you'd see is in readability. In your simple example, you can see how async makes the program a tiny bit simpler:

static async Task simpleAsync()
{
    int i = await jobAsync();
    Console.WriteLine("Async done. Result: " + i.ToString());
}

static Task simpleTask()
{
    return jobAsync().ContinueWith(i => 
        Console.WriteLine("Async done. Result: " + i.ToString()));
}

But where things get really gnarly is when you're dealing with logical branches and error handling. This is where async/await really shine:

static async Task simpleAsync()
{
    int i;
    try
    {
        i = await jobAsync();
    } 
    catch (Exception e)
    {
        throw new InvalidOperationException("Failed to execute jobAsync", e);
    }
    Console.WriteLine("Async done. Result: " + i.ToString());
    if(i < 0)
    {
        throw new InvalidOperationException("jobAsync returned a negative value");
    }
    await doSomethingWithResult(i);
}

You might be able to come up with non-async code that mimics the behavior of this method, but it would be really ugly, and step-through debugging in the IDE would be difficult, and the stack traces wouldn't show you as neatly exactly what was happening when exceptions are thrown. That's the kind of behind-the-scenes magic that async just kind of magically takes care of for you.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • 1
    OP could have written `simpleTask().Wait()` and not needed the `Console.ReadLine()`. I'm not sure your point is relevant – Kevin Gosse Aug 20 '19 at 21:44