【发布时间】:2019-12-26 00:33:49
【问题描述】:
背景
Async/Await 通过自动创建“状态机”来促进 .NET 中的响应式应用程序,从而使应用程序的主线程即使在执行阻塞工作时也能保持响应式。
Windows 窗体、WPF 和 ASP.NET(据我所知)都包含了 SynchronizationContext 的一种形式(尽管 ASP.NET 最近可能已将其删除;我并不肯定,因为我不使用它。)
我最近需要扩展一个 Windows 窗体应用程序以支持从命令行接受参数,并且在这样做时发现 Async/Await 停止工作。在我的应用程序执行了一些(几乎是随机的)步骤后,它会挂起或返回到不正确的点,从而有效地停止。
SynchronizationContext
经过研究,我发现在幕后,Async/Await 依赖于 SynchronizationContext 来有效地处理路由机器状态(如上所述)。不清楚的是没有 SynchronizationContext 会发生什么:Stephen Toub(在他的博客上) post here) 表明 Async/Await 将执行,但没有线程关联,并且没有 SynchronizationContext,Async/Await 最终会在随机线程上执行。
Stephen 继续解释“AsyncPump.cs”,他的类用于实现控制台应用程序的 SynchronizationContext,并在测试 AsyncPump 时,到目前为止,它是成功的。
问题
- Stephen 的帖子来自 2012 年;还有其他解决方案吗?也许他的 AsyncPump 类已被集成(和/或修改)到更新版本的 .NET 中?如果可用,我更愿意使用库指定的等效项,这样如果 Async/Await 的底层实现发生任何更改,它也会自动更新,就像 WindowsFormsSynchronizationContext 一样。李>
- 我可以安全地使用 WindowsFormsSynchronizationContext 吗?在 Program.cs 中,我正在确定是否要实例化并打开一个表单,使用 Application.Run() 来执行此操作,它会自动为我设置一个 SynchronizationContext(以及消息泵等)我尝试实例化一个 WindowsFormsSynchronizationContext 并使用 SynchronizationContext.SetSynchronizationContext() 在我的主线程上设置它,虽然它可以编译,但我遇到了与根本没有 SynchronizationContext 时相同的问题。
我正在寻找在控制台应用程序中支持 Async/Await 的最佳实践,因为(据我所知)它肯定需要 SynchronizationContext 才能正确执行。
编辑 1:添加伪代码以帮助说明场景
如果我的程序接收到多个参数,我假设它是从命令提示符调用的,并创建了一个自定义“MyCustomConsole”类,该类使用 P/Invoke 到 Win32 来调用 AttachConsole(-1)。此时,我可以从 CLI 读取/写入,因为我的程序是控制台应用程序。如果我没有收到任何额外的参数,那么我可以按预期启动 Windows 窗体 GUI ("Application.Run(new Form1());")。
问题是我最终调用的代码来执行阻塞操作(“RunBlockingOperationsAsync()”)是异步/等待保持响应,当通过 GUI 调用时(通过“Application.Run()”),工作美好的。如果我尝试在没有“Application.Run()”的情况下调用“RunBlockingOperationsAsync”,程序会在调试时死锁或跳转到意外区域,从而导致崩溃。
我尝试实现 WindowsFormsSynchronizationContext,但以同样的方式失败。但是,利用 Stephen Toub 的“AsyncPump.cs”解决方案可以解决问题(见下文。)
为此必须有一个内置的 .NET 框架,对吗?如果没有控制台应用程序的默认实现,我无法相信 Async/Await 可以如此彻底地实现。我目前的理解是,如果没有 Stephen 的“AsyncPump.cs”类(或类似类),控制台应用程序中的 Async/Await 利用率将无法正确执行;实际上,这使得在控制台应用程序中使用 Async/Await 在默认情况下无法按原样使用。
似乎控制台应用程序应该有一个等效版本的“Application.Run()”,它初始化一个适当的 SynchronizationContext(以及其他任何可能需要的东西——现在可能什么都没有。)
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading; // <-- Note that System.Threading is required for SynchronizationContext.
namespace WindowsFormsApp1
{
static class Program
{
/// <summary>
/// The main entry point for the application—NOTE this is the default WinForms implementation for 'Program.cs'.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
MainAsync();
}
private static async Task MainAsync()
{
// If the application has received more than one argument, assume it's been invoked from the Command Prompt.
if (Environment.GetCommandLineArgs().Count() > 1)
{
using (MyCustomConsole mcc = new MyCustomConsole())
{
SynchronizationContext sctx = SynchronizationContext.Current; // <-- Initializes sctx to NULL, as at this point in the program,
// there is no SynchronizationContext. It is initialized when
// "Application.Run()" is invoked.
// Doesn't work (no SynchronizationContext):
await mcc.Run(); // <-- If the MyCustomConsole class is invoked without using AsyncPump.cs,
// it has no SynchronizationContext, and without it, Async/Await operations can
// execute on any thread from the ThreadPool, which causes deadlocks and jumping
// (almost at random?) to unexpected parts of my program, which I can only attribute
// to the size of the program and including numerous nested Async/Await calls, depending
// on what the program is trying to do.
// Perhaps instantiate a WindowsFormsSynchronizationContext and use it?
SynchronizationContext.SetSynchronizationContext = new WindowsFormsSynchronizationContext();
await mcc.Run(); // <-- Also fails in the same manner as above, despite having a SynchronizationContext.
// I don't understand why.
AsyncPump.Run(async () => { await mcc.Run(); }); // <-- This works. AsyncPump.cs is the custom SynchronizationContext that
// Stephen Toub provided in his blog. It not only handles SynchronizationContext,
// but sets itself as the SynchronizationContext for the current thread, which
// is required for Async/Await to operate with thread affinity.
}
}
else // Otherwise, display the main form and operate with a GUI.
{
Application.Run(new Form1()); // <-- Application.Run() instantiates a WindowsFormsSynchronizationContext,
// (amongst other things, like a message pump) and this is vital to a proper
// Async/Await machine state that requires thread affinity.
}
}
}
}
分辨率
这个问题的根源有两个:首先,使用 Async/Await 的开发人员应该了解 Async/Await 的实现可能会因 SynchronizationContext 的不同而有所不同; Stephen Toub does an excellent job explaining here. 了解控制台应用程序在默认情况下没有特定的 SynchronizationContext,因此会将延续发布到 ThreadPool。如果你调试一个 Console 应用,你会发现监控 SynchronizationContext.Current 是 NULL。
其次,认识到(对于 Windows 窗体)Application.Run() 设置了消息泵和单线程 SynchronizationContext。在 Application.Run() 之后监视 SynchronizationContext.Current 将返回一个 WindowsFormsSynchronizationContext 对象。感谢@noseratio,我了解到实例化 Windows 窗体 UserControl 对象也会实例化并设置 SynchronizationContext.Current 以使用新的 WindowsFormsSynchronizationContext,但仅如果它为 NULL首先。
这解释了我的问题:我正在处理的应用程序是 Windows 窗体应用程序,通常启动时,Application.Run() 用于调用消息泵并设置 WindowsFormsSynchronizationContext。异步/等待完美运行。但是,在添加对 CLI 的支持时,我实例化了一个派生自 UserControl 的对象。一旦我实例化它,我以前的 NULL SynchronizationContext 现在是一个 WindowsFormsSynchronizationContext,现在 Async/Await 延续被发布到它而不是 ThreadPool - 在实例化新的 SynchronizationContext 后 ThreadPool 上的延续会发生什么,我不能说。我遇到了不稳定的程序行为,通常要么是“等待 Task.Delay()”调用无限期挂起,要么是我的应用程序(在调试器中)的控制看似随意地跳来跳去。据报道,设置 (WindowsFormsSynchronizationContext.AutoInstall = false) 应该可以防止自动将 NULL SynchronizationContext 替换为 WindowsFormsSynchronizationContext,但在我的测试中,它仍然被替换(并且 Async/Await 仍然中断。)
我没有使用 WPF 对此进行测试,但我希望 WPF 的行为类似(和/或开发人员将面临类似的问题。)
有多种解决方案:
在我看来,最好的解决方案是在 CLI 模式下执行时不实例化 Windows 窗体 UserControl(或 WPF 等效项),如果可以的话。如果可能,将工作抽象到它自己的类中,并将 UserControls(及其等价物)留给 View 抽象。这允许 Async/Await 在应用程序需要的任何同步上下文上运行:如果是 Windows 窗体,则为 WindowsFormsSynchronizationContext。如果是 WPF,则为 Dispatcher (?) SynchronizationContext。如果是控制台应用程序,它会在 ThreadPool 而不是 SynchronizationContext 上运行。
显式设置自己的 SynchronizationContext:@Stephen Toub 的 AsyncPump 类;或@Stephen Cleary 的 AsyncContext 类;或@TheodorZoulias 的任何一个解决方案都有效(在我的测试中)。可能有充分的理由在#1 上使用这些解决方案之一,例如,您可能正在处理控制台应用程序,但别无选择,只能实例化 WinForms UserControl,或者也许使用您不知道的在幕后执行此操作的库。如果遇到这种情况,我建议在应用程序的各个阶段监控 SynchronizationContext.Current。
【问题讨论】:
-
您的意思是在不带参数调用时正确运行的同一可执行文件在带参数调用时停止正确运行?在这两种情况下你都打电话给
Application.Start吗? -
没有办法用参数调用可执行文件会导致这种情况。你能发布你的
Program.Main吗? -
扩展 Windows 窗体应用程序以支持从命令行接受参数 - 它仍然是同一个进程,还是需要在多个进程实例之间进行通信?
-
我的意思是
Application.Run(),而不是Application.Start()。 -
控制台应用程序通常不需要设置 SynchronizationContext 来让 async/await 正常工作。
标签: c# .net async-await command-line-interface command-prompt