(This question isn't a duplicate of Why do we need the async keyword? - it's more of the opposite: I'm not questioning the
async keyword - I'm asking if compilers could elide the use of
await completely behind the scenes, making async code syntactically identical to synchronous code)
await keyword in many languages provides a succinct way to describe a continuation or for constructing coroutines - but I've wondered if it was necessary at all, as in situations where I've used it, the compiler should be smart enough to know when a task/promise/future should be awaited or not: by deferring any
await until the
Task is consumed as though it were awaited.
As an example, consider this async C# code that runs two Tasks concurrently:
Task<Foo> fooTask = GetFooAsync(); Task<Bar> barTask = GetBarAsync(); DoSomethingElseSynchronously(); Foo foo = await fooTask; Bar bar = await barTask; Foo foo2 = foo.Clone(); DoSomething( foo2, foo, bar );
I was thinking that the compiler (or rather, some static-analysis code rewriter that runs before the real C#-to-IL compiler) could allow it to be written like so:
Foo foo = GetFooAsync(); Bar bar = GetFooAsync(); DoSomethingElseSynchronously(); Foo foo2 = foo.Clone(); DoSomething( foo2, foo, bar );
await would result in the above code being the same as though it were written like this:
Task<Foo> fooTask = GetFooAsync(); Task<Bar> barTask = GetBarAsync(); DoSomethingElseSynchronously(); Foo foo2 = (await fooTask).Clone(); DoSomething( foo2, (await fooTask), await barTask );
Of course this only works if the re-inserted
await is both idempotent (which is ostensibly is, at least with the stock
Task<T> in .NET) and side-effect free (so reordering the
await statements should not affect the correctness of the program).
I imagine most async C# code tends to immediately
await a Task because most async APIs do not support concurrent operations on the same resources, so you must
await one operation before starting another on the same object (e.g.
DbContext doesn't support multiple concurrent queries, and
FileStream requires each async read or write operation to be completed before starting another - though I might be wrong if
FileSteam fully supports Windows' Overlapped IO functionality) - there's no built-in way in .NET for an asynchronous API to declare support for concurrent operations but a simple addition to the
Task API could enable this.
Another example of concurrent async operations is firing off a batch of HTTP requests using a single
HttpClient instance - for example a web-crawler might work like this:
List<Uri> uris = ... HttpClient httpClient = ... List<Task<List<Uri>>> tasks = uris .Select( u => httpClient.GetAsync() /* Returns HttpResponseMessage */ ) .Select( response => ReadPageUrisAsync( response ) /* Returns Task<List<Uri>> */ ) .ToList(); List<Uri> foundUris = ( await Task.WhenAll( tasks ) ) .SelectMany( uris => uris ) .Distinct() .ToList();
await were syntactically elided, the compiler should be smart enough to infer that
Task.WhenAll( tasks ) call-site expected an awaited return value because the following Linq expression only works if the
IEnumerable<T> source parameter for
.SelectMany is a
List<List<Uri>> and not a
List<TaskList<List<Uri>>> (I do appreciate this kind of type-inference is a hard problem - I'm using it as a contrived example).
So - assuming that a program's asynchronous operations can be safely awaited out-of-order, is there a reason or situation where
await couldn't be syntactically elided?