为什么不可以在控制台应用程序的“main”方法上指定“异步”修饰符?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (10)
  • 关注 (0)
  • 查看 (200)

我刚开始用async修改器进行异步编程,并且我想知道如何确保我的控制台应用程序的Main方法实际上是异步运行的。

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = bs.GetList();
    }
}

public class Bootstrapper {

    public async Task<List<TvChannel>> GetList()
    {
        GetPrograms pro = new GetPrograms();

        return await pro.DownloadTvChannels();
    }
}

我知道这不是从“顶部”异步运行的,由于无法指定async上的修饰符Main方法,那么我应该如何在main方法处异步运行代码?

提问于
用户回答回答于

在VS11中,编译器将不允许一个async Main方法,在VS2010中,这与Async CTP 是允许的。

尤其是异步/等待异步控制台程序,以下为介绍文章中的一些背景信息:

如果“await”未完成的话,那么它将以异步的方式运行。当它完成时,它会告诉你如何运行剩余的方法,然后从async方法返回。当它将剩下的方法传递给awaitable时,await也会捕获当前的上下文。

稍后,当等待完成时,它将执行其余的async方法(在捕获的上下文内)。

以下是带有async Main的控制台程序出现问题的原因:

异步方法在完成之前会返回给调用者,这在UI应用程序(该方法刚刚返回到UI事件循环)和ASP.NET应用程序(该方法返回线程但保持请求存活)中正常工作。 这对于控制台程序来说效果不是很好:main返回到操作系统 - 所以你的程序会退出。

还有一种解决方案是提供上下文 - 控制台程序的“主循环”是异步兼容的。

如果你有一台具有异步ctp的机器,则可以在My Documents\Microsoft Visual Studio Async CTP\Samples(C# Testing) Unit Testing\AsyncTestUtilities处使用GeneralThreadAffineContext

你可以使用my Nito.AsyncEx NuGet package中的AsyncContext

以下为使用AsyncContext的例子,用法和GeneralThreadAffineContext相同:

using Nito.AsyncEx;
class Program
{
    static void Main(string[] args)
    {
        AsyncContext.Run(() => MainAsync(args));
    }

    static async void MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

或者你只需阻止主控制台线程,直到异步工作完成为止:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }

    static async Task MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

注意GetAwaiter().GetResult()的用法;如果你使用Wait()或者是Result的话请避免出现AggregateException

从VisualStudio2017 Update 3(15.3)开始,只要返回的是Task或者是Task<T>,该语言能支持async Main,所以你可以按照以下的代码来执行:

class Program
{
    static async Task Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

语义与阻塞主线程的GetAwaiter().GetResult()风格相同。但是,C#7.1目前还没有语言规范,所以这只是一个假设。

用户回答回答于

在main 方法处修改对GetList的调用:

Task.Run(() => bs.GetList());
用户回答回答于
用户回答回答于

C#7.1(使用VS 2017 update 3)引入了async main

   static async Task Main(string[] args)
  {
    await ...
  }

了解更多细节:C#7系列,async main

可能会返回一个编译error:

程序不包含适合入口点的静态“main”方法。

错误原因为vs2017.3默认配置为c#7.0而不是c#7.1

你应该显式地修改项目的设置来修改c#7.1特性

可以通过两种方法来设置c#7.1:

方法1:使用项目设置窗口:

  • 打开项目的设置
  • 选择Build选项卡
  • 单击“高级”按钮
  • 选择你想要的版本,如下图所示:

方法2:手动修改PropertyGroup of.csproj

添加此属性:

    <LangVersion>7.1</LangVersion>

例子:

    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <OutputPath>bin\Debug\</OutputPath>
        <DefineConstants>DEBUG;TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
        <Prefer32Bit>false</Prefer32Bit>
        <LangVersion>7.1</LangVersion>
    </PropertyGroup>    
用户回答回答于

最后的代码如下:

private static int Main(string[] args)
{
    var source = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        source.Cancel();
    };

    try
    {
        return MainAsync(args, source.Token).GetAwaiter().GetResult();
    }
    catch (OperationCanceledException)
    {
        return 1223; // Cancelled.
    }
}

private static async Task<int> MainAsync(string[] args, CancellationToken token)
{
    // Your code...

    return await Task.FromResult(0); // Success.
}
用户回答回答于

当我使用控制台应用程序进行快速测试并需要异步时,我通过以下代码来解决:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).Wait();
    }

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}
用户回答回答于

在C#7.1中,你可以运行一个适当的异步Main函数,main函数处有一个扩展方法:

public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);

假设你运行了以下代码:

static async Task Main(string[] args)
{
    Bootstrapper bs = new Bootstrapper();
    var list = await bs.GetList();
}

在编译时,异步入口点方法将被转换为GetAwaitor().GetResult()

详情:https://blogs.msdn.microsoft.com/ma州/2017/05/30/c-7-series-part-2-async-main

要启用C#7.1语言特性,你需要右键单击项目并单击“Properties”,然后转到“Build”选项卡。在此处单击底部的高级按钮:

从语言版本下拉菜单中,选择“7.1”(或任何更高的值):

默认值是“最新的主要版本”,它将默认(在撰写本文时)到c# 7.0,它不支持控制台应用程序中的async main。

用户回答回答于

TPL中最重要的事情之一是取消支持,而控制台应用程序有一种内置的取消方法(CTRL+C),可以很容易地就将他们绑定在一起。以下为构造所有异步控制台应用程序的方式:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();

    System.Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        cts.Cancel();
    };

    MainAsync(args, cts.Token).Wait();
}

static async Task MainAsync(string[] args, CancellationToken token)
{
    ...
}
用户回答回答于

你可以在不需要外部库的情况下执行以下操作:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var getListTask = bs.GetList(); // returns the Task<List<TvChannel>>

        Task.WaitAll(getListTask); // block while the task completes

        var list = getListTask.Result;
    }
}
用户回答回答于

你可以通过以下构造方法来解决问题:

class Program
{
    static void Main(string[] args)
    {
        Task.Run(async () =>
        {
            // Do any async anything you need here without worry
        }).GetAwaiter().GetResult();
    }
}

这将把你在ThreadPool上做的所有事情放到你想要的地方(所以你开始/等待的其他任务的时候不会尝试重新加入他们不存在的线程),然后等到所有操作完成后才关闭控制台应用程序,不需要特殊的循环或外部库。

扫码关注云+社区