-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Description
Async Main
Summary
Allow Main methods to be marked async
, allowing the use of await
inside of them.
Motivation
Many programs (especially small ones) are written in a way that is almost entirely asynchronous. However, currently, programmers need to manually create a bridge between synchronous code
(starting at Main because Main is required to be synchronous), and the deeper levels of their application code (which might be using async APIs). That is to say, their code must -- at some level -- block while waiting for a result in order to go from being asynchronous to synchronous.
There are a subset of programs that contain this bridge inside the Main method itself.
static async Task DoWork() {
await ...
await ...
}
static void Main() {
DoWork().GetAwaiter().GetResult();
}
Doing this by hand is a waste of time and can be a cause of bugs. In the above example, writing DoWork().Wait()
would wrap any thrown exceptions in an AggregateException. Writing DoWork()
with no Wait or GetResult may result in the program shutting down before execution of the asynchronous task is finished.
Instead of making the user hand-write this transfer, we would like to have the compiler do this bridging manually by allowing main methods to be marked as async
.
Detailed Design
The following signatures are currently invalid entrypoints, but would become valid if this speclet were to be implemented.
async Task Main()
async Task<int> Main()
async Task Main(string[])
async Task<int> Main(string[])
Task Main()
Task<int> Main()
Task Main(string[])
Task<int> Main(string[])
Because the CLR doesn't accept async methods as entrypoints, the C# compiler would generate a synthesized
main method calls the user-defined async Main.
- For async-Main definitions that return
Task
, a synthesizedstatic void Main
is generated. - For async-Main definitions that return
Task<int>
, a synthesizedstatic int Main
is generated. - For async-Main definitions that take an argument array, the synthesized main method will take an argument
array as well; passing it on to the user-defined async Main.
The body of the synthesized Main method contains a call to the user-defined async Main on the receiving end of .GetAwaiter().GetResult()
.
An example transformation is provided below.
async Task<int> Main(string[] args) {
// User code goes here
}
int $GeneratedMain(string[] args) {
return Main(args).GetAwaiter().GetResult();
}
There is one back-compat issue that we need to worry about. As @gafter showed in his example, old code using traditional Main alongside a new async Main will be problematic. Previously, his code would produce warnings, but with async Main being a valid entrypoint, there would be more than one valid entrypoint found, resulting in an error.
The proposed solution to this would be that the compiler only looks for an async Main after not finding any synchronous Mains. If a synchronous Main is found, then warnings are issued for any async Main that is found afterward.
Drawbacks
- Doubles the amount of valid entry-point signatures.
- The existence of
async Main
might encourage inexperienced programmers to try to use async on
more methods than are necessary, because they don't need to think about the transition from sync to async
anymore.
Benefits
- Brings compiled C# closer to C# scripting. (C# scripting allows for
await
in their "main" context) - Makes it easier to use C# in small programs that call async methods without resorting to blocking.
- People are already writing by hand what we would like to expose to them via the language. We can make sure
that it's done correctly.
API Impact
When a compilation is asked for viable entrypoints, any user-defined async Main methods will be returned. The synthesized "real" entry point will be invisible.
Unresolved Questions
- Should we allow
async void Main
? If so, what would the synthesized version do?