ChatGPT解决这个技术问题 Extra ChatGPT

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

我是使用 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 中异步运行代码?

在 C#7.1 中不再是这种情况。主要方法可以是异步的
这是C#7.1 blog post announcement。请参阅标题为 Async Main 的部分。

L
Liam

正如您所发现的,在 VS11 中,编译器将不允许使用 async Main 方法。在带有异步 CTP 的 VS2010 中,这是允许的(但从不推荐)。

2017 年 11 月 30 日更新: 从 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/awaitasynchronous console programs 的博文。以下是介绍帖子中的一些背景信息:

如果 "await" 看到 awaitable 还没有完成,那么它会异步执行。它告诉 awaitable 在完成时运行该方法的其余部分,然后从 async 方法返回。 Await 还将在将方法的其余部分传递给可等待对象时捕获当前上下文。稍后,当 awaitable 完成时,它将执行 async 方法的其余部分(在捕获的上下文中)。

这就是在带有 async Main 的控制台程序中出现问题的原因:

请记住,在我们的介绍文章中,异步方法将在完成之前返回给它的调用者。这在 UI 应用程序(该方法只返回 UI 事件循环)和 ASP.NET 应用程序(该方法从线程返回但保持请求活动)中完美运行。对于控制台程序来说效果不是很好:Main 返回操作系统 - 所以你的程序退出。

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

如果您有一台带有 Async 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 包装。


可以使用简单的 WaitResult,这并没有错。但请注意,有两个重要区别:1) 所有 async 延续都在线程池而不是主线程上运行,2) 任何异常都包装在 AggregateException 中。
直到这个(和你的博客文章)才解决这个问题。这是迄今为止解决此问题的最简单方法,您可以在 nuget 控制台中安装该软件包,只需一个“install-package Nito.Asyncex”即可。
我对这些解决方案的问题是调试器(Visual Studio 2017 15.3.1)在 Main 方法中中断,而不是在抛出异常的位置,并且“调用堆栈”窗口为空。
C# 7.1 现在有一个 async main,可能值得添加到您的好答案中,@StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
如果您在 VS 2017 中使用 C# 7.1 版本,我需要通过在 csproj 文件中添加 <LangVersion>latest</LangVersion> 作为 shown here 来确保项目配置为使用最新版本的语言。
C
Chris Moschini

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

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

这会将您所做的一切放在您想要的线程池上(因此您启动/等待的其他任务不会尝试重新加入他们不应该的线程),并等到一切都完成后再关闭控制台应用程序。不需要特殊的循环或外部库。

编辑:合并 Andrew 针对未捕获异常的解决方案。


这种方法非常明显,但往往会包装异常,所以我现在正在寻找更好的方法。
@abatishchev您应该在代码中使用try/catch,至少在Task.Run中使用,如果不是更精细的话,不要让异常浮到Task中。通过将 try/catch 放在可能失败的事物周围,您将避免总结问题。
如果您将 Wait() 替换为 GetAwaiter().GetResult(),您将在抛出异常时避免使用 AggregateException 包装器。
在撰写本文时,这就是在 C# 7.1 中引入 async main 的方式。
@user9993 根据 this proposal,这并不完全正确。
p
p.campbell

您也可以通过执行以下操作来完成此操作而无需外部库:

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

请记住,getListTask.Result 也是一个阻塞调用,因此上面的代码可以在没有 Task.WaitAll(getListTask) 的情况下编写。
此外,如果 GetList 抛出,您必须捕获 AggregateException 并询问其异常以确定实际抛出的异常。但是,您可以调用 GetAwaiter() 来获取 TaskTaskAwaiter,然后在其上调用 GetResult(),即 var list = getListTask.GetAwaiter().GetResult();。从 TaskAwaiter(也是一个阻塞调用)获取结果时,抛出的任何异常都不会包含在 AggregateException 中。
.GetAwaiter().GetResult 是我需要的答案。这非常适合我试图做的事情。我可能也会在其他地方使用它。
M
Mahmoud Al-Qudsi

在 C# 7.1 中,您将能够执行正确的 async 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();
}

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

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

编辑:

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

https://i.stack.imgur.com/bxXL4.png

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

https://i.stack.imgur.com/7R7zk.png

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


FWIW 这在 Visual Studio 15.3 及更高版本中可用,目前可从此处以 beta/预览版的形式提供:visualstudio.com/vs/preview
等一下……我正在运行一个完全更新的安装,我的最新选项是 7.1……你是怎么在 5 月获得 7.2 的?
可能的答案是我的。在我认为 7.2(预览版?)可能已经发布时,10 月的编辑是由其他人完成的。
注意-检查它是否在所有配置上,而不仅仅是在执行此操作时进行调试!
@user230910 谢谢。 c# 团队最奇怪的选择之一。
C
Cory Nelson

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

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

取消令牌是否也应该传递给 Wait()
不,因为您希望异步代码能够优雅地处理取消。如果您将它传递给 Wait(),它不会等待异步代码完成——它会停止等待并立即结束进程。
您确定吗?我刚刚尝试过,似乎取消请求正在最深层次处理,即使 Wait() 方法传递了相同的令牌。我想说的是,它似乎没有任何区别。
我确定。您想取消操作本身,而不是等待操作完成。除非您不关心清理代码的完成或其结果。
是的,我想我明白了,它似乎对我的代码没有任何影响。让我偏离正轨的另一件事是 ReSharper 礼貌地提示支持取消的等待方法;)您可能希望在示例中包含一个 try catch,因为它会抛出一个 OperationCancelledException,我一开始无法弄清楚
M
M.Hassan

C# 7.1(使用 vs 2017 更新 3)引入了 async main

你可以写:

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

更多详情C# 7 Series, Part 2: Async Main

更新:

你可能会得到一个编译错误:

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

此错误是由于 vs2017.3 默认配置为 c#7.0 而不是 c#7.1。

您应该明确修改项目的设置以设置 c#7.1 功能。

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

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

打开项目的设置

选择构建选项卡

单击高级按钮

选择你想要的版本如下图所示:

https://i.stack.imgur.com/dKR3U.png

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

Ş
Şafak Gür

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

像约翰说的那样有一个异步任务 MainAsync 。

调用它的 .GetAwaiter().GetResult() 来捕捉 do0g 所说的底层异常。

像 Cory 说的那样支持取消。

第二个 CTRL+C 应立即终止该过程。 (谢谢宾基!)

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

许多不错的程序只会在第一次取消 CancelKeyPress 时,如果您按 ^C 一旦您获得正常关机,但如果您不耐烦,第二次 ^C 会不正常地终止。使用此解决方案,如果程序未能兑现 CancellationToken,您需要手动终止该程序,因为 e.Cancel = true 是无条件的。
J
Johan Falk

还不需要这么多,但是当我使用控制台应用程序进行快速测试并需要异步时,我刚刚解决了这个问题:

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

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

如果您必须将任务调度到当前上下文然后等待它,则此示例将出错(例如,您可能忘记添加 ConfigureAwait(false),因此返回方法将被调度到主线程中,该线程在等待函数中)。因为当前线程处于等待状态,你会收到死锁。
不正确,@ManushinIgor。至少在这个简单的示例中,没有与主线程关联的 SynchronizationContext。所以它不会死锁,因为即使没有 ConfigureAwait(false),所有的延续都将在线程池上执行。
u
user3408030

对于从 Main 异步调用任务,请使用

Task.Run() for .NET 4.5 Task.Factory.StartNew() for .NET 4.0 (可能需要 Microsoft.Bcl.Async 库用于 async 和 await 关键字)

详细信息:http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx


F
Fredrik Ljung

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

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

J
Jon Skeet

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

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

真正想做什么?请注意,您的 GetList() 方法目前确实不需要异步 - 它无缘无故地添加了一个额外的层。它在逻辑上等价于(但更复杂):

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

乔恩,我想异步获取列表中的项目,那么为什么异步不适合 GetList 方法?是因为我需要收集异步列表中的项目而不是列表本身吗?当我尝试用 async 标记 Main 方法时,我得到一个“不包含静态 Main 方法......”
@danielovich:DownloadTvChannels() 返回什么?大概它返回一个 Task<List<TvChannel>> 不是吗?如果没有,您不太可能等待它。 (可能,考虑到等待者模式,但不太可能。)至于 Main 方法 - 它仍然需要是静态的...您是否将 static 修饰符替换async 修饰符也许?
是的,它返回一个 Task<..> 就像你说的。无论我如何尝试将 async 放在 Main 方法签名中,它都会引发错误。我坐在 VS11 预览位上!
@danielovich:即使返回类型为 void?只是public static async void Main() {}?但是如果 DownloadTvChannels() 已经返回了一个 Task<List<TvChannel>>,那么它可能已经是异步的了 - 所以你不需要添加另一个层。值得仔细理解这一点。
@nawfal:回首往事,我认为它在 VS2013 发布之前发生了变化。不确定 C# 7 是否会改变这一点……
K
Kedrzu

最新版本的 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)
    {
         (...)
    }

T
Theodor Zoulias

从 C# 7.1 开始,以下签名对 Main 方法有效。

public static void Main() { }
public static int Main() { }
public static void Main(string[] args) { }
public static int Main(string[] args) { }
public static async Task Main() { }
public static async Task<int> Main() { }
public static async Task Main(string[] args) { }
public static async Task<int> Main(string[] args) { }

所以,现在你可以做 async/await

static async Task Main(string[] args)
{
    Console.WriteLine("Hello Asyn Main method!");
    await Task.Delay(200);
}

C
Community

在 MSDN 上,Task.Run Method (Action) 的文档提供了这个示例,该示例展示了如何从 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

请注意示例后面的语句:

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

因此,如果您希望任务在主应用程序线程上运行,请参阅 @StephenClearythe answer

关于任务运行的线程,还要注意斯蒂芬在他的回答中的comment

您可以使用简单的等待或结果,这并没有错。但请注意,有两个重要区别:1) 所有异步延续都在线程池而不是主线程上运行,以及 2) 任何异常都包装在 AggregateException 中。

(有关如何结合异常处理来处理 AggregateException,请参阅 Exception Handling (Task Parallel Library)。)

最后,在 Task.Delay Method (TimeSpan) 文档中的 MSDN 上,此示例显示了如何运行返回值的异步任务:

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 函数,而不是将 delegate 传递给 Task.Run

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

u
user_v

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

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

N
Nathan Phillips

为了避免在调用堆栈中某处尝试重新加入当前线程(卡在等待中)的函数时冻结,您需要执行以下操作:

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() 的死锁。等等,这个答案应该有更多的赞成票......
K
Kristaps Koks
class Program
{
     public static EventHandler AsyncHandler;
     static void Main(string[] args)
     {
        AsyncHandler+= async (sender, eventArgs) => { await AsyncMain(); };
        AsyncHandler?.Invoke(null, null);
     }

     private async Task AsyncMain()
     {
        //Your Async Code
     }
}

T
Tom Charles Zhang

这是假设的,但我在想:

static void Main(string[] args)
{
    var context = new Thread(() => /*do stuff*/);
    context.Start();
    context.Join();
}