gpt4 book ai didi

c# - 调用Application.Run()或实例化WinForms UserControl对象时,在.NET Console中使用Async/Await中断应用程序

转载 作者:行者123 更新时间:2023-11-30 19:35:23 26 4
gpt4 key购买 nike

背景

Async / Await通过自动创建“状态机”来促进.NET中响应式应用程序的使用,即使执行阻塞工作,该应用程序的主线程也可以保持响应式。

Windows Forms,WPF和ASP.NET(据我所知)都包含一种SynchronizationContext形式(尽管ASP.NET最近可能已删除了它;我并不肯定,因为我不使用它。)

最近,我需要扩展Windows Forms应用程序以支持从命令行接受参数,并且这样做时发现Async / Await停止了工作。在我的应用程序中执行了一些(几乎是随机的)步骤后,它要么挂起,要么返回到错误的点,从而有效地停止了运行。

同步上下文

经过研究,我发现在幕后,Async / Await依靠SynchronizationContext有效地处理路由机器状态(如上所述)。目前尚不清楚没有SynchronizationContext会发生什么情况:Stephen Toub(在他的博客文章 )表示将执行Async / Await,但是没有线程亲和力,并且没有SynchronizationContext,Async / Await可能最终在随机线程上执行。

Stephen继续讲解“ AsyncPump.cs”,这是他的用于为控制台应用程序实现SynchronizationContext的类,并且到目前为止,在测试AsyncPump方面已经取得了成功。

问题


斯蒂芬的职位是2012年;还有其他解决方案吗?也许他的AsyncPump类已集成(和/或修改)到最新版本的.NET中?我宁愿使用一个库指定的等效项(如果有的话),这样,如果Async / Await的幕后实现发生任何更改,它也将自动更新,就像WindowsFormsSynchronizationContext一样。
我可以安全地使用WindowsFormsSynchronizationContext吗?在Program.cs中,我正在确定是否要实例化并打开一个Form,使用Application.Run()这样做,它会自动为我处理SynchronizationContext的设置(以及消息泵等)。我尝试实例化WindowsFormsSynchronizationContext并使用SynchronizationContext.SetSynchronizationContext()在我的主线程上进行设置,尽管可以编译,但我遇到的问题与根本没有SynchronizationContext时一样。


我正在寻找在控制台应用程序中支持Async / Await的最佳实践,因为(据我所知)它肯定需要SynchronizationContext才能正确执行。



编辑1:添加伪代码以帮助说明这种情况

如果我的程序收到了多个参数,则假定已从命令提示符处调用了它,并创建了一个自定义“ MyCustomConsole”类,该类使用P / Invoke Win32来调用AttachConsole(-1)。此时,由于我的程序是一个控制台应用程序,因此可以从CLI读取/写入。如果没有收到任何其他参数,则可以按预期启动Windows Form GUI(“ Application.Run(new Form1());”)。

问题是我最终调用以执行阻止操作的代码(“ RunBlockingOperationsAsync()”)是Async / Await以便保持响应,并且在通过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的开发人员应了解,根据SynchronizationContext的不同,Async / Await的实现可能有所不同。 here了解默认情况下控制台应用程序没有特定的SynchronizationContext,因此将继续发布到ThreadPool中。如果调试控制台应用程序,则会发现监视SynchronizationContext.Current为NULL。

其次,认识到(对于Windows窗体)Application.Run()设置了消息泵和单线程SynchronizationContext。在Application.Run()之后监视SynchronizationContext.Current将返回WindowsFormsSynchronizationContext对象。感谢@noseratio,我了解到,实例化Windows Forms UserControl对象还将实例化并设置SynchronizationContext.Current以使用新的WindowsFormsSynchronizationContext,但前提是必须以NULL开头。

这就解释了我的问题:我正在处理的应用程序是Windows Forms应用程序,通常启动时,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 Forms UserControl(或等效的WPF)。将抽象工作放到自己的类中,如果可能的话,将UserControls(及其等效项)留给View抽象。这允许Async / Await在您的应用程序需要的任何同步上下文上运行:如果是Windows窗体,则为WindowsFormsSynchronizationContext。如果为WPF,则为Dispatcher(?)SynchronizationContext。如果是控制台应用程序,则它在ThreadPool而不是SynchronizationContext上运行。
明确设置自己的SynchronizationContext:@Stephen Toub的AsyncPump类;或@Stephen Cleary的AsyncContext类;或@TheodorZoulias的两个解决方案都有效(在我的测试中。)也许有充分的理由在#1上使用这些解决方案之一,例如,您可能正在使用Console应用程序,但别无选择,只能实例化WinForms UserControl,或者使用您不知道的在后台进行操作的库。如果遇到这种情况,我建议在应用程序的各个阶段监视SynchronizationContext.Current。

最佳答案

在没有同步上下文的情况下(或使用默认的SyncrhonizationContext时),await延续通常可以同步运行,即在其先前任务已结束的同一线程上运行。这可能导致模糊的死锁,这是.NET Framework 4.6中引入TaskContinuationOptions.RunContinuationsAsynchronously的原因之一。有关更多详细信息和示例,请查看此博客文章:The danger of TaskCompletionSource class

AsyncPump阻止代码挂起的事实表明您在mcc.Run()内部可能存在类似情况。由于AsyncPumpawait延续施加了真正的异步性(尽管在同一线程上),因此减少了发生死锁的机会。

就是说,我不建议使用AsyncPumpWindowsFormsSynchronizationContext作为解决方法。相反,您应该尝试找出导致代码挂起的确切原因(以及挂起的位置),然后在本地解决该问题,例如只需用Task.Run包装有问题的电话。

我可以在您的代码中发现的另一个问题是,您不必等待或等待MainAsync返回的任务。因此,至少对于逻辑的控制台分支(尤其是不使用AsyncPump的情况),您的程序可能会过早地结束,具体取决于mcc.Run()中的内容,并且您可能会让某些异常不被观察到。

关于c# - 调用Application.Run()或实例化WinForms UserControl对象时,在.NET Console中使用Async/Await中断应用程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57579612/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com