入口点可以在 CoreCLR 上用 'async' 修饰符标记吗?

Entry point can be marked with the 'async' modifier on CoreCLR?

在 Stephan Cleary 最近关于 Async Console Apps on .NET CoreCLR 的博客 post 中,他向我们展示了在 CoreCLR 中(目前 运行ning on Visual Studio 2015,CTP6),入口点 "Main"实际上可以标记为async Task,正确编译实际上是运行:

public class Program
{
    public async Task Main(string[] args)
    {
        Console.WriteLine("Hello World");
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine("Still here!");
        Console.ReadLine();
    }
}

给出以下输出:

ASP.NET 团队 A Deep Dive into the ASP.NET 5 Runtime 的博客 post 加强了这一点:

In addition to a static Program.Main entry point, the KRE supports instance-based entry points. You can even make the main entry point asynchronous and return a Task. By having the main entry point be an instance method, you can have services injected into your application by the runtime environment.

到目前为止我们都知道,An entry point cannot be marked with the 'async' modifier。那么,在新的 CoreCLR 运行 时代,这实际上是如何实现的?

深入CoreCLR运行时的源码,我们可以看到一个名为RuntimeBootstrapper的静态class,它负责调用我们的入口点:

public static int Execute(string[] args)
{
    // If we're a console host then print exceptions to stderr
    var printExceptionsToStdError = Environment.GetEnvironmentVariable(EnvironmentNames.ConsoleHost) == "1";

    try
    {
        return ExecuteAsync(args).GetAwaiter().GetResult();
    }
    catch (Exception ex)
    {
        if (printExceptionsToStdError)
        {
            PrintErrors(ex);
            return 1;
        }

        throw;
    }
}

我们可以看到,在内部,它调用 ExecuteAsync(args).GetAwaiter().GetResult();,这在语义上等同于调用 Task.Result,只是我们接收的不是包装的 AggregationException,而是未包装的异常。

理解这一点很重要,因为 没有 "black magic" 它是如何发生的。对于当前版本的 CoreCLR 运行时,该方法被允许标记为 async Task,因为它被运行时阻止在调用链的更高层。

旁注:

深入研究 ExecuteAsync,我们会看到它最终会调用:

return bootstrapper.RunAsync(app.RemainingArguments);

When looking inside,我们看到实际 MethodInfo 调用我们的入口点:

public static Task<int> Execute(Assembly assembly, string[] args, IServiceProvider serviceProvider)
{
    object instance;
    MethodInfo entryPoint;

    if (!TryGetEntryPoint(assembly, serviceProvider, out instance, out entryPoint))
    {
        return Task.FromResult(-1);
    }

    object result = null;
    var parameters = entryPoint.GetParameters();

    if (parameters.Length == 0)
    {
        result = entryPoint.Invoke(instance, null);
    }
    else if (parameters.Length == 1)
    {
        result = entryPoint.Invoke(instance, new object[] { args });
    }

    if (result is int)
    {
        return Task.FromResult((int)result);
    }

    if (result is Task<int>)
    {
        return (Task<int>)result;
    }

    if (result is Task)
    {
        return ((Task)result).ContinueWith(t =>
        {
            return 0;
        });
    }

    return Task.FromResult(0);
}