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 Task
s 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 Task
s 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.