Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Main [Speclet] #7476

Closed
TyOverby opened this issue Dec 14, 2015 · 16 comments
Closed

Async Main [Speclet] #7476

TyOverby opened this issue Dec 14, 2015 · 16 comments

Comments

@TyOverby
Copy link
Contributor

TyOverby commented Dec 14, 2015

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 synthesized static void Main is generated.
  • For async-Main definitions that return Task<int>, a synthesized static 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?
@TyOverby
Copy link
Contributor Author

CC @dotnet/roslyn-compiler

@gafter
Copy link
Member

gafter commented Dec 14, 2015

This would be a breaking change. The following program produces only a warning today, but it would be an error under this proposal.

using System.Threading.Tasks;

namespace CSharpSample1
{
    class Program
    {
        static int Main(string[] args)
        {
            return Main().GetAwaiter().GetResult();
        }

        static Task<int> Main()
        {
            throw null;
        }
    }
}

@TyOverby
Copy link
Contributor Author

@gafter: Looks like that's correct.

There's three options that I can see:

  1. Have this be a breaking change.
  2. Build this program like it has been building previously by detecting that one of the Main methods is async, so it should prefer the other one. (Also issue a new warning)
  3. Not land the feature.

Obviously, I'd prefer option 2 (unless there are any other cleaner solutions).

Right now we fail with an error if there are more than one "viable" entry point methods. Going with option two means that we would introduce preference. Not something to take lightly, but maybe worth it in this scenario? Most people that hit this edge case would likely want to use this feature.

Update: we are going with option two, I'll update the speclet.

@gafter
Copy link
Member

gafter commented Dec 14, 2015

Yes, I think option (2) makes sense. Formally, we would say that a method with one of the Task return types is not a candidate to be a "main method" unless there is no candidate under the old rules.

And we should check with the whole LDM to make sure we have consensus on this (or some other) approach.

/cc @MadsTorgersen

@gafter
Copy link
Member

gafter commented Dec 14, 2015

/cc @stephentoub As the champion for this feature, how do you suggest we handle this?

@TyOverby
Copy link
Contributor Author

@gafter I have updated the Detailed Design section of the speclet to include the resolution to your example program.

@svick
Copy link
Contributor

svick commented Dec 15, 2015

A small clarification:

In the above example, writing DoWork().Wait() would silently ignore thrown exceptions.

No, it wouldn't ignore them, calling Wait() on a failed Task also throws. The reason why .GetAwaiter().GetResult() is preferable is because it directly throws the exception, while .Wait() throws it wrapped in AggregateException.

@TyOverby
Copy link
Contributor Author

@svick: thanks for the feedback, I've updated that section.

@antiufo
Copy link

antiufo commented Dec 16, 2015

What about including a SynchronizationContext? One thing I like about async is that you have much less worries about race conditions, as everything in an async flow happens in a specific thread/sync context.
Personally, I start most of my console apps with

static void Main()
{
    SingleThreadSynchronizationContext.Run(MainAsync)
}

Would it be possible to include a similar class in coreclr/corefx that takes care of setting up a proper sync context, and using it in the generated Main()?

@antiufo
Copy link

antiufo commented Dec 16, 2015

Here's an example: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx
I personally use a modified version of that in my console applications.

@TyOverby
Copy link
Contributor Author

TyOverby commented Jan 4, 2016

@antiufo

This is something that would certainly be nice to have, but is out of scope of this speclet.

Also, I don't see how you would go about adding synchronization contexts to the proposed syntax.

@stephentoub
Copy link
Member

Point of order: Why is this a separate issue from #1695? The speclet seems largely a duplicate (though with a few additional points) of my comments from #1695 (comment). Regardless...

As the champion for this feature, how do you suggest we handle this?

I'm fine with the approach: "Yes, I think option (2) makes sense. Formally, we would say that a method with one of the Task return types is not a candidate to be a "main method" unless there is no candidate under the old rules."

This is something that would certainly be nice to have, but is out of scope of this speclet.

As I noted in #1695, I do not believe the language feature should do anything with regards to synchronization context, in particular because a) that brings a ton of policy into the language, and b) there are many variations on what that policy could be. If a developer wants to layer on that support, they can do so manually. There's a separate question (unrelated to Roslyn) of whether corefx should provide any additional types to help with such an implementation, noting that it already provides types that would enable serialized execution, e.g. ConcurrentExclusiveSchedulerPair.

@TyOverby
Copy link
Contributor Author

TyOverby commented Jan 5, 2016

Why is this a separate issue

I wanted to collect all the information about the feature in one spot so it could be easily reviewed by the LDM.

@gafter gafter modified the milestones: 2.1, 1.3 Jul 19, 2016
@acelent
Copy link

acelent commented Nov 3, 2016

Allow Main methods to be marked async, allowing the use of await inside of them.

The method should be named MainAsync to follow the current naming convention for asynchronous methods and to avoid further overloading the allowed Main entry point signatures.

This way, we only leave open if it should be the runtime to also look for MainAsync entry points or if it should be the compiler to create the respective bridge blocking Main entry point in case there's none.

Many programs (especially small ones) are written in a way that is almost entirely asynchronous.

For such small programs, if you have both synchronous and asynchronous APIs available, the asynchronous ones will only generate overhead. If you only have asynchronous APIs, you only save 1 to 4 lines of code:

    // C# 6
    static int Main(string[] args) => MainAsync(args).GetAwaiter().GetResult();

    // 1 line, if code formatting rules allow it
    static int Main(string[] args) { return MainAsync(args).GetAwaiter().GetResult(); }

    // 4 lines, if code formatting rules force it
    static int Main(string[] args)
    {
        return MainAsync(args).GetAwaiter().GetResult();
    }

For not so small programs, this should be the least of your troubles.

Doing this by hand is a waste of time and can be a cause of bugs.

Really?

In the above example, writing DoWork().Wait() would wrap any thrown exceptions in an AggregateException.

What's the big deal? There's no requirement that the host handles specific exceptions differently, or that it outputs the thrown exception at all, much less that it formats the exception and inner exceptions in a certain way.

If you care, you should seriously log and/or handle exceptions, either with language constructs (try/catch) or with the AppDomain.UnhandledException event. If you don't, it doesn't matter.

I prefer .GetAwaiter().GetResult() myself, and I find no trouble in writing it when I need it instead of .Result and .Wait(). I mean, it's not that often that you actually need it.

Writing DoWork() with no Wait or GetResult may result in the program shutting down before execution of the asynchronous task is finished.

I honestly expect anyone to notice this during development.

Drawbacks -- the amount of valid entry-point signatures.

I agree. Also, see my first reply regarding the asynchronous entry point method's name.

Drawbacks -- 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.

Actually, I don't really understand the issue here. There will always be a sync-to-async transition, whether you see it or not.

In your opinion, on what methods would async not be necessary? I've stated that small applications probably have no benefit, so I gather that you agree up to some point.

Benefits -- Brings compiled C# closer to C# scripting. (C# scripting allows for await in their "main" context)

I agree. This consistency is probably the only reasonable argument. However, it's not a substantial saving.

Benefits -- Makes it easier to use C# in small programs that call async methods without resorting to blocking.

But it will block You just don't see it.

We should probably skip this. Asynchronous code should not be taken so lightly. At any point, at least one non-background thread must be alive to keep the program running. You should make sure how this is implemented yourself. And if you do, you can make interesting things like creating a CancellationTokenSource that is cancelled on e.g. Ctrl+c in console applications, message loop exit in Windows Forms or WPF, or service stop request in a Windows service, where you pass the Token to MainAsync and all the way down to every asynchronous I/O calls, directly or through CancellationTokenSource.CreateLinkedTokenSource.

In case this goes forward, do the MTAThread and STAThread attributes matter? Should we always use the default SynchronizationContext, which is MTAThread friendly, or switch to a generic context-bound synchronization context when the STAThread attribute is applied?

@TyOverby TyOverby modified the milestones: Unknown, 2.1 Feb 3, 2017
@TyOverby TyOverby removed their assignment Feb 7, 2017
@imanushin
Copy link

In addition: possible, it is better to use main thread in thread pool for this case? So we will not have "sleeping" thread.

@gafter
Copy link
Member

gafter commented Mar 27, 2017

We are now tracking this proposal at dotnet/csharplang#97. It is championed by @stephentoub .

@gafter gafter closed this as completed Mar 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants