我是使用 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 更新 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 还没有语言规范,所以这只是一个假设。
我最近发表了关于 async/await 和 asynchronous 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
包装。
你可以用这个简单的结构来解决这个问题:
class Program
{
static void Main(string[] args)
{
Task.Run(async () =>
{
// Do any async anything you need here without worry
}).GetAwaiter().GetResult();
}
}
这会将您所做的一切放在您想要的线程池上(因此您启动/等待的其他任务不会尝试重新加入他们不应该的线程),并等到一切都完成后再关闭控制台应用程序。不需要特殊的循环或外部库。
编辑:合并 Andrew 针对未捕获异常的解决方案。
Wait()
替换为 GetAwaiter().GetResult()
,您将在抛出异常时避免使用 AggregateException
包装器。
async main
的方式。
您也可以通过执行以下操作来完成此操作而无需外部库:
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()
来获取 Task
的 TaskAwaiter
,然后在其上调用 GetResult()
,即 var list = getListTask.GetAwaiter().GetResult();
。从 TaskAwaiter
(也是一个阻塞调用)获取结果时,抛出的任何异常都不会包含在 AggregateException
中。
在 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 语言功能,您需要右键单击项目并单击“属性”,然后转到“构建”选项卡。在那里,单击底部的高级按钮:
https://i.stack.imgur.com/bxXL4.png
从语言版本下拉菜单中,选择“7.1”(或任何更高的值):
https://i.stack.imgur.com/7R7zk.png
默认值为“最新的主要版本”,它将评估(在撰写本文时)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)
{
...
}
Wait()
?
Wait()
,它不会等待异步代码完成——它会停止等待并立即结束进程。
Wait()
方法传递了相同的令牌。我想说的是,它似乎没有任何区别。
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>
如果您使用的是 C# 7.1 或更高版本,请使用 nawfal's answer,只需将 Main 方法的返回类型更改为 Task
或 Task<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.
}
e.Cancel = true
是无条件的。
还不需要这么多,但是当我使用控制台应用程序进行快速测试并需要异步时,我刚刚解决了这个问题:
class Program
{
static void Main(string[] args)
{
MainAsync(args).Wait();
}
static async Task MainAsync(string[] args)
{
// Code here
}
}
SynchronizationContext
。所以它不会死锁,因为即使没有 ConfigureAwait(false)
,所有的延续都将在线程池上执行。
对于从 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
在 Main 中尝试将对 GetList 的调用更改为:
Task.Run(() => bs.GetList());
在引入 C# 5 CTP 时,您当然可以将 Main 标记为 async
...尽管这样做通常不是一个好主意。我相信这已被 VS 2013 的发布改变为一个错误。
除非您已启动任何其他 foreground 线程,否则您的程序将在 Main
完成时退出,即使它启动了一些后台工作。
您真正想做什么?请注意,您的 GetList()
方法目前确实不需要异步 - 它无缘无故地添加了一个额外的层。它在逻辑上等价于(但更复杂):
public Task<List<TvChannel>> GetList()
{
return new GetPrograms().DownloadTvChannels();
}
DownloadTvChannels()
返回什么?大概它返回一个 Task<List<TvChannel>>
不是吗?如果没有,您不太可能等待它。 (可能,考虑到等待者模式,但不太可能。)至于 Main
方法 - 它仍然需要是静态的...您是否将 static
修饰符替换为 async
修饰符也许?
public static async void Main() {}
?但是如果 DownloadTvChannels()
已经返回了一个 Task<List<TvChannel>>
,那么它可能已经是异步的了 - 所以你不需要添加另一个层。值得仔细理解这一点。
最新版本的 C# - C# 7.1 允许创建异步控制台应用程序。要在项目中启用 C# 7.1,您必须将 VS 至少升级到 15.3,并将 C# 版本更改为 C# 7.1
或 C# 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);
}
在 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 的 the 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;
});
在我的情况下,我有一个我想从我的主要方法异步运行的作业列表,已经在生产中使用它很长一段时间并且工作正常。
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();
}
不定期副业成功案例分享
Wait
或Result
,这并没有错。但请注意,有两个重要区别:1) 所有async
延续都在线程池而不是主线程上运行,2) 任何异常都包装在AggregateException
中。<LangVersion>latest</LangVersion>
作为 shown here 来确保项目配置为使用最新版本的语言。