无法在控制台应用程序的'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个回答

4
从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) { }

现在你可以使用异步/等待功能。

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

3
当C# 5 CTP被引入时,你确实可以使用async标记Main方法...尽管这通常不是一个好主意。我相信随着VS 2013的发布,这一点已经发生了改变,成为了一个错误。
除非你启动了其他前台线程,否则当Main完成时,你的程序将退出,即使它启动了一些后台工作。
你真正想做什么?请注意,你的GetList()方法目前并不需要异步-它增加了一个额外的层次没有真正的原因。它在逻辑上等同于(但比较复杂):
public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}

2
Jon,我想异步获取列表中的项目,那为什么GetList方法不适合使用async呢?这是因为我需要异步收集列表中的项目而不是列表本身吗?当我尝试使用async标记Main方法时,我会得到一个“does not contain a static Main method…”的错误。 - danielovich
@danielovich:DownloadTvChannels()返回什么?它很可能返回一个Task<List<TvChannel>>,不是吗?如果不是,你就不能等待它。(虽然有awaiter模式,但这种情况不太可能发生。)至于Main方法——它仍然需要是静态的……你是不是用async修饰符替换了static修饰符? - Jon Skeet
是的,无论如何都不起作用!这就是为什么我尝试在上面再做一个抽象的原因... - danielovich
@JonSkeet “它增加了一个没有实际意义的额外层” 如果我在“Main”中使用“Task.Wait()”,那么是否有理由在整个控制台应用程序中使用async/await?例如,如果您使用仅支持异步的库,如“HttpClient”,那么您是否应该使所有调用方法也是异步的,并在“Main”中使用“Task.Wait()”,或者您应该直接在调用“HttpClient”的第一个方法中使用“Task.Wait()”? - Niklas Peter
1
@nawfal:回头看,我认为这个变化是在VS2013发布之前发生的。不确定C# 7是否会改变这一点... - Jon Skeet
显示剩余8条评论

3
在Main中将GetList的调用更改为:
Task.Run(() => bs.GetList());

2
在我的情况下,我有一份工作列表,我想从我的主方法异步运行这些工作,已经在生产环境中使用了相当长的时间,效果很好。
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 ...
}

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

关于任务运行的线程,请注意Stephen在他的答案中发表的评论

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

(请参见任务并行库中的异常处理,了解如何加入异常处理以处理AggregateException。)

最后,在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函数而不是将代理传递给Task.Run:

(注意,在lambda函数中添加您需要运行的代码)

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

0
为避免在调用堆栈深处的某个函数尝试重新加入当前线程(该线程正陷入等待状态)时出现冻结,您需要执行以下操作:
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().Wait死锁,这个答案应该有更多的赞... - Stefano d'Antonio

-1
以下代码可用于使主函数异步。我已经对其进行了调整,以使用长时间运行的任务(在此处了解更多:https://learn.microsoft.com/en-us/dotnet/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;
        
    }

-1

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

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

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

-1

这只是假设,但我在思考:

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

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