无法在控制台应用程序的'Main'方法上指定'async'修饰符

571

我对带有async修饰符的异步编程很陌生。 我正在尝试弄清楚如何确保我的控制台应用程序的Main方法实际上是异步运行的。

我对使用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();
    }
}

我知道这个程序不是从"顶部"异步运行的。由于无法在Main方法上指定async修饰符,那么我如何在main中异步运行代码呢?


42
C#7.1已经不再这样了。主方法可以是异步的,但意思不变。 - Vasily Sliounaiev
4
这里是C#7.1博客公告,请查看标题为**异步主函数(Async Main)**的部分。 - styfle
20个回答

540
在Visual Studio 2012中,编译器会禁止使用async Main方法。这在Visual Studio 2010中使用Async CTP时是允许的(但从未推荐过)。
从Visual Studio 2017更新3(15.3)开始,语言现在支持async Main - 只要它返回TaskTask<T>。所以现在你可以这样做:
class Program
{
    static async Task Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

语义似乎与阻塞主线程的GetAwaiter().GetResult()方式相同。然而,目前还没有C# 7.1的语言规范,所以这只是一种假设。
我有关于async/await异步控制台程序的博客文章。以下是介绍文章中的一些背景信息:

如果"await"发现可等待对象尚未完成,则它会以异步方式执行。它告诉可等待对象在完成时运行方法的剩余部分,然后从异步方法中返回。当传递方法的剩余部分给可等待对象时,await还会捕获当前的上下文

稍后,当可等待对象完成时,它将在捕获的上下文中执行异步方法的剩余部分。

这就是为什么在具有async Main的控制台程序中会出现问题的原因:

记住我们在介绍帖子中提到的,异步方法会在完成之前返回给调用者。这在UI应用程序中非常完美(方法只是返回到UI事件循环),以及ASP.NET应用程序中(方法返回到线程之外,但保持请求活动)。但对于控制台程序来说,情况就不太理想了:Main方法返回给操作系统,所以你的程序就退出了。
一个解决方案是为控制台程序提供自己的上下文 - 一个与异步兼容的“主循环”。
如果你的机器上安装了Async CTP,你可以使用"My Documents\Microsoft Visual Studio Async CTP\Samples(C# Testing) Unit Testing\AsyncTestUtilities"中的"GeneralThreadAffineContext"。或者,你可以使用我的Nito.AsyncEx NuGet包中的"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包装。

36
你可以使用简单的WaitResult,这没有任何问题。但是请注意两个重要的区别:1)所有的async继续运行在线程池而不是主线程上,2)任何异常都会被包装在AggregateException中。 - Stephen Cleary
3
在看了这个(和你的博客文章)之前,我一直在苦恼如何解决这个问题。这绝对是解决这个问题最简单的方法,而且你只需要在nuget控制台中输入“install-package Nito.Asyncex”就可以安装该软件包了。 - ConstantineK
1
我对这些解决方案的问题是调试器(Visual Studio 2017 15.3.1)在Main方法中中断,而不是在抛出异常的位置中断,且调用堆栈窗口为空。 - Greg
10
C# 7.1 现在支持 async main, 可能值得将其添加到您出色的答案中,@StephenCleary https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/async-main.md - Mafii
3
如果你正在使用 VS 2017 中的 C# 7.1 版本,我需要确保项目配置为使用最新版本的语言,方法是在 csproj 文件中添加 <LangVersion>latest</LangVersion>,具体步骤可参考这里 - Liam
显示剩余11条评论

393

您可以使用这个简单的结构来解决这个问题:

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

这将把您所做的所有事情放在 ThreadPool 上,这是您想要的(以便您启动/等待的其他任务不尝试重新加入不应该的线程),并在关闭控制台应用程序之前等待所有事情完成。无需特殊循环或外部库。

编辑:包括安德鲁的未捕获异常解决方案。


3
这种方法非常明显,但往往会包含异常情况,所以我现在正在寻找更好的方法。 - abatishchev
2
@abatishchev 在你的代码中应该使用try/catch,至少在Task.Run内部使用,如果可能的话更加细致。不要让异常浮现到Task上面。通过将try/catch放置在可能失败的事物周围,可以避免包装问题。 - Chris Moschini
57
如果你用GetAwaiter().GetResult()代替Wait(),当出现异常时就能避免AggregateException的封装。 - Andrew Arnott
7
截至目前,这就是C# 7.1中引入“async main”的方式。 - user9993
根据这个提案,@user9993所说的并不完全正确。 - Sinjai

92

你可以通过以下方法完成此操作,而无需使用外部库:

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;
    }
}

7
请记住,getListTask.Result 也是一个阻塞调用,因此上述代码可以在不使用 Task.WaitAll(getListTask) 的情况下编写。 - do0g
28
另外,如果 GetList 抛出异常,你必须捕获 AggregateException 并查询其内部的异常以确定实际抛出的异常。但是,你可以调用 GetAwaiter() 获取 TaskTaskAwaiter,并在其上调用 GetResult(),例如:var list = getListTask.GetAwaiter().GetResult();。当从 TaskAwaiter 中获取结果时(这也是一个阻塞调用),任何抛出的异常都不会被包装在 AggregateException 中。 - do0g
2
".GetAwaiter().GetResult是我需要的答案。它完美地解决了我尝试做的事情。我可能也会在其他地方使用它。" - Deathstalker

91

在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();
}

编译时,async入口方法将被转换为调用GetAwaitor().GetResult()

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

编辑:

要启用C# 7.1语言功能,您需要右键单击项目并单击“属性”,然后转到“生成”选项卡。 在那里,单击底部的高级按钮:

enter image description here

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

enter image description here

默认值是“最新主要版本”,这将评估为C# 7.0,该版本不支持控制台应用程序中的async main。


2
这是提供给 Visual Studio 15.3 及以上版本的功能,目前作为测试版/预览版发布在此处:https://www.visualstudio.com/vs/preview/ - Mahmoud Al-Qudsi
等一下...我正在运行完全更新的安装程序,我的最新选项是7.1...你怎么在五月份就已经得到了7.2? - user1228
我的回答是我自己的。十月份的编辑是由其他人完成的,当时我认为可能已经发布了7.2(预览版?)。 - nawfal
例如,请查看此GitHub存储库:https://github.com/nicknijenhuis/ChromeCastBackgroundSyncer - Nick N.
1
注意 - 当你这样做时,请检查它是否在所有配置上,而不仅仅是调试。 - user230910
1
@user230910 谢谢。这是 C# 团队做出的最奇怪的选择之一。 - nawfal

83

我会添加一个重要的功能,其他答案都忽略了:取消支持。

在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).GetAwaiter.GetResult();
}

static async Task MainAsync(string[] args, CancellationToken token)
{
    ...
}

1
取消令牌也应该传递给Wait()吗? - Siewers
5
不行,因为你希望异步代码能够优雅地处理取消操作。如果将其传递给Wait(),它不会等待异步代码完成,而是立即停止等待并结束进程。 - Cory Nelson
你确定吗?我刚试了一下,似乎取消请求正在最深层级别上进行处理,即使Wait()方法传递了相同的令牌。 我的意思是,看起来没有任何区别。 - Siewers
4
我确定你想取消操作本身,而不是等待操作完成。除非你不关心清理代码是否完成或其结果。 - Cory Nelson
1
是的,我想我明白了,只是在我的代码中似乎没有任何区别。另一件让我偏离轨道的事情是ReSharper有关等待方法支持取消的提示;) 你可能需要在示例中包含try catch,因为它会抛出OperationCancelledException异常,这一开始我无法理解。 - Siewers
@Siewers 在你的代码中,你可能正在同步处理取消操作。有些情况下,令牌会被取消,但在抛出“OperationCanceledException”之前,你仍然会等待某些任务完成。如果你这样做了,你应该会注意到行为上的差异,因为将令牌传递给Wait()会导致它在程序的Task完成之前中止。 - binki

27
C# 7.1(使用VS 2017更新3)引入了async Main
你可以这样写:
static async Task Main(string[] args)
{
    await ...
}

更多详细信息请参见C# 7 系列,第2部分:异步主函数

更新:

您可能会遇到编译错误:

程序不包含适用于入口点的静态 'Main' 方法

这个错误是由于vs2017.3默认配置为c#7.0而不是c#7.1引起的。

您应该显式修改项目的设置以启用c#7.1功能。

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

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

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

enter image description here

方法2:手动修改.csproj文件的PropertyGroup 在.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>

21

如果你正在使用C# 7.1或更高版本,请参考nawfal的回答,只需将Main方法的返回类型更改为TaskTask<int>。如果不是:

最终的代码如下:
private static int Main(string[] args)
{
    var cts = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = !cts.IsCancellationRequested;
        cts.Cancel();
    };

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

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

    return await Task.FromResult(0); // Success.
}

2
许多好的程序只会在第一次按下^C时取消CancelKeyPress,这样如果您按下^C一次,就会得到一个优雅的关闭,但是如果您不耐烦地再按下第二个^C,则会不优雅地终止。使用此解决方案,如果程序未能遵守CancellationToken,您需要手动终止程序,因为e.Cancel = true是无条件的。 - binki

18

到目前为止,我还没有太多需求,但是当我需要在控制台应用程序中进行快速测试并且需要异步时,我通常会这样解决:

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

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}

如果您需要将任务调度到当前上下文并等待它(例如,您可能会忘记添加ConfigureAwait(false)),那么此示例将无法正常工作。因为当前线程处于等待状态,所以您将收到死锁错误。在等待函数中,返回方法将被调度到主线程中,从而导致问题。 - Manushin Igor
7
不正确,@ManushinIgor。至少在这个简单的例子中,主线程没有与任何SynchronizationContext相关联。因此即使没有使用ConfigureAwait(false),所有的后续操作也会在线程池上执行,因此不会发生死锁。 - Andrew Arnott

6

5
最新版本的C# - C# 7.1可以创建异步控制台应用程序。要在项目中启用C# 7.1,您需要将VS升级到至少15.3,并将C#版本更改为C# 7.1C#最新次要版本。要执行此操作,请转到项目属性 -> 建立 -> 高级 -> 语言版本。
完成后,以下代码将起作用:
internal class Program
{
    public static async Task Main(string[] args)
    {
         (...)
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接