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.