不能指定'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中异步运行代码?

283256 次浏览

当c# 5 CTP被引入的时候,你肯定会用可以标记Main和async…尽管这样做通常不是个好主意。我相信这是在VS 2013的发布中改变的,成为一个错误。

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

你EYZ1想做什么?注意,你的GetList()方法目前真的不需要是异步的——它添加了一个额外的层,没有真正的原因。它在逻辑上等价于(但比):

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

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

从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还没有语言规范,所以这只是一个假设。


我最近有关于异步/等待异步控制台程序的博客文章。以下是介绍帖子中的一些背景信息:

如果“await"看到可等待对象还没有完成,那么它就进行异步操作。它告诉可等待对象在方法完成时运行其余部分,然后从异步方法运行返回。当Await将方法的剩余部分传递给可等待对象时,它还将捕获当前的上下文

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

下面是为什么在使用async Main的控制台程序中这是一个问题:

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

一个解决办法是提供你自己的上下文——“主循环”。对于异步兼容的控制台程序。

如果你有一台使用异步CTP的机器,你可以从我的文档\Microsoft Visual Studio Async CTP\样本(c#测试)单元测试\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包装。

你也可以在不需要外部库的情况下做到这一点,方法如下:

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

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

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

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

  1. Task.Run()用于。net 4.5

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

< p >细节: # EYZ0 < / p >

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

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)
{
MainAsync(args).Wait();
}


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

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

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

当你在调用堆栈的某个地方调用一个函数,试图重新加入当前线程(被卡在等待中)时,为了避免冻结,你需要做以下事情:

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

(转换仅用于解决歧义)

在MSDN上,的任务。运行方法(动作)的文档提供了这个示例,展示了如何从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

请注意下面的例子:

示例显示异步任务执行在不同的

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

关于任务运行的线程,也注意Stephen在他的回答上的评论:

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

. #中

(参见异常处理(任务并行库)了解如何结合异常处理来处理AggregateException。)


最后,在MSDN的的任务。延迟方法(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函数,而不是传递delegateTask.Run:

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

如果你使用的是c# 7.1或更高版本,使用nawfal的回答并将Main方法的返回类型更改为TaskTask<int>。如果你不是:

  • 来个async Task MainAsync 就像约翰说的
  • 调用它的.GetAwaiter().GetResult()来捕获底层异常就像狗狗说的
  • 支持取消就像科里说的
  • 第二个CTRL+C应该立即终止进程。(谢谢# EYZ1 !)
  • 处理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.
}

在c# 7.1中,你将能够做一个正确的异步主要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()

细节:# EYZ0

编辑:

要启用c# 7.1语言特性,您需要在项目上右键单击“Properties”,然后转到“Build”选项卡。在那里,点击底部的高级按钮:

enter image description here

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

enter image description here

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

最新版本的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(使用vs 2017更新3)引入了async main

你可以这样写:

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

更多详细信息c# 7系列,第2部分:Async Main

更新:

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

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

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

您应该显式地修改项目的设置来设置c#7.1特性。

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

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

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

enter image description here

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

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

这是假设,但我在想:

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

不确定这是否是你要找的,但我想等待加载方法。我最终使用main_shows处理程序,并使它异步:

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