繁体   English   中英

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

[英]Can't specify the 'async' modifier on the 'Main' method of a console app

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

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

2017 年 11 月 30 日更新从 Visual Studio 2017 Update 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" 看到 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的 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包装。

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

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

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

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

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

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

在 C# 7.1 中,您将能够执行正确的async 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/mazhou/2017/05/30/c-7-series-part-2-async-main

编辑:

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

在此处输入图像描述

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

在此处输入图像描述

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

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

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

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:

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

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

在此处输入图像描述

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

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

  • 像约翰所说的那样有一个async Task MainAsync
  • 调用它的.GetAwaiter().GetResult()来捕捉像 do0g 所说的底层异常。
  • 支持像科里所说的取消。
  • 第二个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.
}

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

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

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

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

  1. .NET 4.5 的 Task.Run()

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

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

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

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

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

从 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# 5 CTP 被引入时,你当然可以async标记 Main ... 尽管这样做通常不是一个好主意。 我相信这已被 VS 2013 的发布改变为一个错误。

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

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

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

在 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

请注意示例后面的语句:

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

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

关于任务运行的线程,还要注意斯蒂芬对他的回答的评论

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

(有关如何合并异常处理来处理AggregateException ,请参阅异常处理(任务并行库) 。)


最后,在 MSDN 上来自Task.Delay Method (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

请注意,不是将delegate传递给Task.Run ,而是可以传递一个 lambda 函数,如下所示:

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

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

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

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

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

(演员只需要解决歧义)

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

这是假设的,但我在想:

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

不确定这是否是您要查找的内容,但我想等待加载方法。 我最终使用了 Main_Shown 处理程序并使其异步:

private async void Main_Shown(object sender, EventArgs e)
{
   await myAsyncMethod();
}

以下代码可用于创建主异步。 我已经调整它以使用长时间运行的任务(在此处了解更多信息: https://learn.microsoft.com/en-us/do.net/api/system.threading.tasks.taskcreationoptions?view.net-7.0

它还从上述响应中实施取消令牌。

    private static int Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        Console.CancelKeyPress += (s, e) =>
        {
            e.Cancel = !cts.IsCancellationRequested;
            cts.Cancel();
            Console.WriteLine("CancellationRequested");
        };

        try
        {
            var task = new Task<int>(
                () => MainAsync(args, cts.Token).GetAwaiter().GetResult(), 
                cts.Token,
                TaskCreationOptions.LongRunning //https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0
            );
            task.Start();
            var exitCode =  task.GetAwaiter().GetResult();      
            /*Or this.*/
            //var exitCode = MainAsync(args, cts.Token).GetAwaiter().GetResult();
            return exitCode;// MainAsync(args, cts.Token).GetAwaiter().GetResult();
        } 
        catch (OperationCanceledException ex)
        {
            Console.WriteLine(ex);
            return 1223; // Cancelled.
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            return -1;
        }
    }
    private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
         await Something()
         return;
    }

在我写的下面的例子中。 您可以使用 maxDegreeOfParallelism 和 numberOfIteration 来了解/查看任务的处理方式。 学习TPL的好起点!

private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
        
        var infos = new ConcurrentBag<Info>();
        var mySuperUselessService = new BigWorkload();

        int numberOfSecond = 1;
        int numberOfIteration = 25;     //Experiment with this
        int maxDegreeOfParallelism = 4; //Experiment with this

        var simulateWorkTime = TimeSpan.FromSeconds(numberOfSecond);
        var informations = Enumerable.Range(1, numberOfIteration)
            .Select(x => new Info() { Index = x });

        var count = informations.Count();
        var chunkNeeded = Math.Round(count / Convert.ToDecimal(maxDegreeOfParallelism), MidpointRounding.ToPositiveInfinity);

        var splashInfo = @$"
Press CTRL + C to cancel. 
Processing {count} items, maxDegreeOfParallelism set to {maxDegreeOfParallelism}.
But it will be bound by the core on the machine {Environment.ProcessorCount}. 
This operation should take ~{chunkNeeded * (numberOfSecond + 0.01m)}s
And will be starting test in 2s
";
        Console.WriteLine(splashInfo);
        await Task.Delay(TimeSpan.FromSeconds(2));

        var parralelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism, CancellationToken = cancellationToken};
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var forLoopTask = Parallel.ForEachAsync(informations, parralelOptions, async (info, token) =>
        {
            await mySuperUselessService.Simulate(simulateWorkTime, info);
            Console.WriteLine(info);
            infos.Add(info);


        });
        await forLoopTask;
        stopwatch.Stop();

        foreach (var grouped in infos.GroupBy(x => x.ManagedThreadId))
        {
            Console.WriteLine($"ThreadId: {grouped.Key}");
            foreach (var item in grouped)
            {
                Console.WriteLine($"\t Index: {item.Index} {item.TaskCurrentId}");
                
            }
        }
        Console.WriteLine($"NumberOfThread: {infos.GroupBy(x => x.ManagedThreadId).Count()}");
        Console.WriteLine($"Elasped: {stopwatch.ElapsedMilliseconds / 1000d}s");
        Console.WriteLine(splashInfo);

        return 0;
        
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM