无法在控制台应用程序的“主要”方法上指定“异步”修饰符


444

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

我知道这不是从“顶部”异步运行的。由于无法asyncMain方法上指定修饰符,因此如何在main异步方式内运行代码?


22
C#7.1中不再是这种情况。主要方法可能是异步的
Vasily Sliounaiev

2
这是C#7.1博客文章的公告。请参阅标题为Async Main的部分。
styfle

Answers:


380

如您所见,在VS11中,编译器将禁止使用async Main方法。在VS2010中,使用Async CTP允许(但绝不建议这样做)。

我最近在博客文章中特别提到了异步/等待异步控制台程序。以下是介绍性帖子的一些背景信息:

如果“ await”看到awaitable尚未完成,则它将异步执行。它告诉可等待的对象在完成时运行该方法的其余部分,然后从异步方法中返回。当等待方法将其余方法传递给等待方法时,它还将捕获当前上下文

稍后,当awaitable完成时,它将执行async方法的其余部分(在捕获的上下文中)。

这就是在控制台程序中使用出现问题的原因async Main

请记住,在我们的介绍性帖子中,异步方法将在完成之前返回其调用方。这在UI应用程序(该方法仅返回到UI事件循环)和ASP.NET应用程序(该方法从线程返回但使请求保持活动状态)中完美地工作。对于Console程序,效果不是很好:Main返回OS-因此程序退出。

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

如果您有一台具有异步CTP的计算机,则可以GeneralThreadAffineContext从“ 我的文档” \“ Microsoft Visual Studio”中,使用“异步CTP” \“样本(C#测试)”单元测试\“ AsyncTestUtilities”。或者,您可以AsyncContext我的Nito.AsyncEx NuGet包中使用

这是一个使用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(); 这样可以避免AggregateException使用Wait()或时发生的换行Result

更新,2017-11-30:从Visual Studio 2017更新3(15.3)开始,该语言现在支持async Main-,只要它返回Task或即可Task<T>。因此,您现在可以执行以下操作:

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

语义似乎与GetAwaiter().GetResult()阻塞主线程的样式相同。但是,尚无C#7.1的语言规范,因此这只是一个假设。


30
可以使用简单的WaitResult,这没有任何问题。但是请注意,有两个重要区别:1)所有async延续都在线程池上而不是在主线程上运行,并且2)任何异常都包装在中AggregateException
Stephen Cleary 2014年

2
在解决这个问题之前(以及您的博客文章)遇到了一个真正的问题。到目前为止,这是解决此问题的最简单方法,只需使用“ install-package Nito.Asyncex”就可以在nuget控制台中安装该软件包。
ConstantineK

1
@StephenCleary:感谢Stephen的快速回复。我不明白为什么有人在抛出异常时希望调试器中断。如果我正在调试并遇到空引用异常,则似乎直接进入有问题的代码行。VS的工作方式类似于“开箱即用”,用于同步代码,但不适用于异步/等待。
格雷格,

6
C#7.1现在具有异步主体,可能值得添加到您的绝佳答案中,@ StephenClearygithub.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…–
Mafii

3
如果您在VS 2017中使用C#7.1版本,则需要通过添加<LangVersion>latest</LangVersion>到csproj文件中来确保将项目配置为使用该语言的最新版本,如此处所示
利亚姆'18

359

您可以使用以下简单结构解决此问题:

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

这会将您执行的所有操作放在所需的ThreadPool上(因此,您启动/等待的其他任务不会尝试重新加入不应执行的线程),并等待所有操作完成后再关闭Console应用程序。不需要特殊的循环或外部库。

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


3
这种方法非常明显,但是倾向于包装异常,因此我现在正在寻找一种更好的方法。
abatishchev

2
@abatishchev您应该在代码中至少在Task中使用try / catch.Run(如果不是更精细的话),不要让异常浮现到Task上。通过对可能失败的事物进行尝试/捕获,可以避免出现问题。
克里斯·莫斯基尼

54
如果用替换Wait()GetAwaiter().GetResult()AggregateException在出现问题时避免使用包装器。
安德鲁·阿诺特

7
async main在撰写本文时,这就是C#7.1中引入的方式。
user9993

@ user9993根据此建议,这并非完全正确。
辛加

90

您还可以通过执行以下操作来执行此操作,而无需外部库:

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

27
另外,如果GetList抛出异常,则必须捕获AggregateException并询问其异常以确定实际抛出的异常。但是,您可以调用GetAwaiter()获取TaskAwaiterTask,然后调用GetResult(),即var list = getListTask.GetAwaiter().GetResult();。从TaskAwaiter(也是阻塞调用)获取结果时,抛出的任何异常都不会包装在中AggregateException
do0g 2014年

1
.GetAwaiter()。GetResult是我需要的答案。这完全可以满足我的尝试。我可能还会在其他地方使用它。

78

在C#7.1中,您将能够执行适当的异步MainMain方法的适当签名已扩展到:

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

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

详细信息:https : //blogs.msdn.microsoft.com/mazhou/2017/05/30/c-7-series-part-2-async-main

编辑:

要启用C#7.1语言功能,您需要右键单击该项目,然后单击“属性”,然后转到“构建”选项卡。在此处,单击底部的高级按钮:

在此处输入图片说明

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

在此处输入图片说明

默认值为“最新主要版本”,它将在撰写本文时评估为C#7.0,该版本在控制台应用程序中不支持异步main。


2
FWIW在Visual Studio 15.3及更高版本中可用,当前可在此处以beta /预览版提供:visualstudio.com/vs/preview
Mahmoud Al-Qudsi

请稍等...我正在运行完全更新的安装,我的最新选择是7.1 ...您如何在5月获得7.2?

可能的答案是我的。我认为7.2(预览版?)可能已经发布时,其他人的十月编辑。
nawfal

1
注意-检查它是否在所有配置上,而不仅仅是在执行此操作时调试!
user230910

1
@ user230910谢谢。C#团队最奇怪的选择之一。
nawfal

74

我将添加一个其他所有答案都忽略的重要功能:取消。

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)
{
    ...
}

取消令牌也应该传递给Wait()吗?
Siewers 2014年

5
不,因为您希望异步代码能够正常处理取消。如果将其传递给Wait(),它将不会等待异步代码完成-它将停止等待并立即结束该过程。
科里·纳尔逊

你确定吗?我刚刚尝试了一下,看来取消请求正在最深层处理,即使Wait()方法传递了相同的令牌也是如此。我想说的是,这似乎没有任何区别。
Siewers 2014年

4
我确定。您要取消操作本身,而不是等待操作完成。除非您不关心清理代码完成或其结果。
科里·尼尔森

1
是的,我想我明白了,它似乎对我的代码没有任何影响。让我偏离路线的另一件事是ReSharper关于支持取消的wait方法的礼貌提示;)您可能想在示例中包括try catch,因为它将抛出OperationCancelledException,我一开始无法弄清楚
Siewers

22

C#7.1(使用vs 2017 Update 3)引入了异步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:使用项目设置窗口:

  • 打开项目的设置
  • 选择构建选项卡
  • 点击高级按钮
  • 选择所需的版本,如下图所示:

在此处输入图片说明

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

添加此属性:

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

20

如果您使用的是C#7.1或更高版本,请使用nawfal的答案,只需将Main方法的返回类型更改为Task或即可Task<int>。如果您不是:

  • 有个async Task MainAsync 喜欢约翰的话
  • 调用它.GetAwaiter().GetResult()以捕获基本异常,如do0g said
  • 支持取消就像Cory所说的
  • 一秒钟CTRL+C应该立即终止该过程。(感谢binki!)
  • 处理OperationCancelledException-返回适当的错误代码。

最终代码如下:

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

1
许多不错的程序只会在第一次时取消CancelKeyPress,因此,一旦按^ C键就可以正常关机,但是如果您不耐烦,第二个^ C会不适当地终止。使用此解决方案,如果由于e.Cancel = true无条件而无法使用CancellationToken,则需要手动终止该程序。
宾基

19

并不需要那么多,但是当我使用控制台应用程序进行快速测试并需要异步时,我就这样解决了它:

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

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

如果必须将任务调度到当前上下文然后等待它,则此示例将工作错误(例如,您可以忘记添加ConfigureAwait(false),因此return方法将被调度到主线程中,该线程在Wait函数中) )。由于当前线程处于等待状态,因此您将收到死锁。
Manushin Igor 2015年

6
不正确,@ ManushinIgor。至少在这个琐碎的示例中,没有SynchronizationContext与主线程相关联。因此,它不会死锁,因为即使没有ConfigureAwait(false),所有延续也将在线程池上执行。
安德鲁·阿诺特 Andrew Arnott),2015年


4

在Main中,尝试将对GetList的调用更改为:

Task.Run(() => bs.GetList());

4

引入C#5 CTP时,您当然可以将Main标记为async ...,尽管这样做通常不是一个好主意。我相信VS 2013的发布已将其更改为错误。

除非您已启动任何其他前台线程,否则Main即使完成一些后台工作,您的程序也会在完成时退出。

什么是你真正想干什么?请注意,您的GetList()方法目前确实不需要异步-它没有任何实际原因添加额外的层。从逻辑上讲,它等效于(但比它复杂):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}

2
乔恩,我想异步获取列表中的项目,那么为什么异步不适用于该GetList方法?是因为我需要异步收集列表中的项目,而不是列表本身?当我尝试用异步方式标记Main方法时,出现“不包含静态Main方法...”
danielovich 2012年

@danielovich:DownloadTvChannels()返回什么?大概它返回Task<List<TvChannel>>不是吗?如果没有,您不太可能等待它。(可能,给定了等待者模式,但不太可能。)至于Main方法-它仍然需要是静态的...您是否static修饰符替换了修饰符async
乔恩·斯基特

是的,它返回一个Task <..>,如您所说。无论我如何尝试在Main方法签名中放置异步,它都会引发错误。我坐在VS11预览版上!
danielovich 2012年

@danielovich:即使返回类型无效?只是public static async void Main() {}吗 但是,如果DownloadTvChannels()已经返回Task<List<TvChannel>>,大概已经是异步的了-因此您无需添加其他层。值得仔细了解这一点。
乔恩·斯基特

1
@nawfal:回想一下,我认为在VS2013发布之前,它发生了变化。不知道C#7是否会改变这一点...
Jon Skeet

4

最新版本的C#-C#7.1允许创建异步控制台应用程序。要在项目中启用C#7.1,您必须将VS至少升级到15.3,并将C#版本更改为C# 7.1C# latest minor version。为此,请转到项目属性->构建->高级->语言版本。

之后,以下代码将起作用:

internal class Program
{
    public static async Task Main(string[] args)
    {
         (...)
    }

3

在MSDN上,Task.Run方法(操作)的文档提供了此示例,该示例显示如何从main以下位置异步运行方法:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        ShowThreadInfo("Application");

        var t = Task.Run(() => ShowThreadInfo("Task") );
        t.Wait();
    }

    static void ShowThreadInfo(String s)
    {
        Console.WriteLine("{0} Thread ID: {1}",
                          s, Thread.CurrentThread.ManagedThreadId);
    }
}
// The example displays the following output:
//       Application thread ID: 1
//       Task thread ID: 3

请注意以下示例后面的语句:

这些示例表明,异步任务在与主应用程序线程不同的线程上执行。

所以,如果不是你想要的主应用程序线程上运行任务时,看到答案通过 @StephenCleary

关于任务运行的线程,还请注意Stephen 对他的回答的评论

可以使用简单的WaitResult,这没有任何问题。但是请注意,有两个重要区别:1)所有async延续都在线程池上而不是在主线程上运行,并且2)任何异常都包装在中AggregateException

(有关如何合并异常处理以处理的信息,请参见异常处理(任务并行库)AggregateException。)


最后,在MSDN上,从Task.Delay方法(TimeSpan)的文档开始,此示例演示如何运行返回值的异步任务:

using System;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        var t = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromSeconds(1.5));
                    return 42;
                });
        t.Wait();
        Console.WriteLine("Task t Status: {0}, Result: {1}",
                          t.Status, t.Result);
    }
}
// The example displays the following output:
//        Task t Status: RanToCompletion, Result: 42

请注意,您可以像这样传递lambda函数,而不是传递给delegateto Task.Run

var t = Task.Run(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(1.5));
            return 42;
        });

1

为了避免在尝试重新连接当前线程(卡在Wait中)的调用堆栈下方某个地方调用函数时冻结,您需要执行以下操作:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        List<TvChannel> list = Task.Run((Func<Task<List<TvChannel>>>)bs.GetList).Result;
    }
}

(仅需使用演员表才能解决歧义)


谢谢; Task.Run不会导致GetList()的死锁。等等,这个答案有更多的争议...
Stefano d'Antonio

1

在我的情况下,我有一个要从主要方法异步运行的作业列表,已经在生产环境中使用了一段时间,并且工作正常。

static void Main(string[] args)
{
    Task.Run(async () => { await Task.WhenAll(jobslist.Select(nl => RunMulti(nl))); }).GetAwaiter().GetResult();
}
private static async Task RunMulti(List<string> joblist)
{
    await ...
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.