调用Application.Run()或实例化WinForms UserControl对象时,在.NET Console中使用Async/Await中断应用程序 [英] Utilizing Async/Await in .NET Console applications breaks when calling Application.Run() or instantiating a WinForms UserControl object

查看:72
本文介绍了调用Application.Run()或实例化WinForms UserControl对象时,在.NET Console中使用Async/Await中断应用程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

Async/Await通过自动创建状态机"来促进.NET中响应式应用程序的使用,即使在执行阻塞工作时,也允许应用程序的主线程保持响应式.

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

我最近需要扩展Windows窗体应用程序,以也支持从命令行接受参数,并且这样做时,发现的Async/Await停止了工作.在进入我的应用程序若干(几乎随机)的步骤后,它要么挂起,要么返回不正确的点,从而有效地暂停.

SynchronizationContext

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

Stephen继续解释"AsyncPump.cs",这是他的类,用于为控制台应用程序实现SynchronizationContext,并且到目前为止,在测试AsyncPump方面,它都已成功.

问题

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

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


添加伪代码以帮助说明这种情况

如果我的程序接收到多个参数,则假定已从命令提示符处调用了该参数,并创建了一个自定义"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的实现可能有所不同; Stephen Toub在这里做了出色的解释.默认情况下,控制台应用程序没有特定的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上的延续发生了什么,我无法说.我遇到了不稳定的程序行为,通常是无限期地挂起"await 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上运行.

  1. 明确设置自己的SynchronizationContext:@Stephen Toub的AsyncPump类;或@Stephen Cleary的AsyncContext类;或@TheodorZoulias的两种解决方案都有效(在我的测试中.)也许有充分的理由在#1上使用其中一种解决方案,例如,您可能正在使用Console应用程序,但是除了实例化WinForms UserControl之外别无选择,或使用您不知道的在后台进行操作的库.如果遇到这种情况,我建议在应用程序的各个阶段监视SynchronizationContext.Current.

解决方案

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

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

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

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

Background

Async/Await facilitates responsive applications in .NET by automatically creating a "state machine", allowing the primary thread of an application to remain responsive even while performing blocking work.

Windows Forms, WPF, and ASP.NET (to my knowledge) all incorporate a form of SynchronizationContext (although ASP.NET may have removed this recently; I'm not positive, as I don't work with it.)

I've recently needed to extend a Windows Forms application to also support accepting arguments from the Command Line, and in doing so, discovered Async/Await stopped working. After some number of (almost random) steps into my application, it would either hang or return to an incorrect point, effectively halting.

SynchronizationContext

After research, I discovered that under the covers, Async/Await relies on a SynchronizationContext to effectively handle routing machine state (as mentioned above.) What wasn't clear is what happened without a SynchronizationContext: Stephen Toub (on his blog post here) indicates that Async/Await will execute, but without thread affinity, and that without a SynchronizationContext, Async/Await can end up executing on random threads.

Stephen goes on to explain "AsyncPump.cs", his class for implementing a SynchronizationContext for console applications, and in testing AsyncPump, so far, it's been successful.

Questions

  1. Stephen's post is from 2012; is there another solution? Perhaps his AsyncPump class has been integrated (and/or modified) into a more recent version of .NET? I would prefer to use an library-designated equivalent, if available, such so that if any changes occur to the under-the-covers implementation of Async/Await, it will automatically be updated as well, like the WindowsFormsSynchronizationContext would be.
  2. Could I safely use the WindowsFormsSynchronizationContext? In Program.cs, I'm determining whether or not I want to instantiate and open a Form, using Application.Run() to do so, which automatically handles setting up a SynchronizationContext for me (as well as message pump, etc.) I tried instantiating a WindowsFormsSynchronizationContext and setting it on my main thread using SynchronizationContext.SetSynchronizationContext(), and although this compiles, I encountered the same problems as when I had no SynchronizationContext at all.

I'm looking for the best practice for supporting Async/Await in a console application, because (as far as I can tell) it definitely needs a SynchronizationContext in order to execute correctly.


Edit 1: Adding pseudocode to help illustrate the scenario

If my program has received more than one argument, I'm assuming that it's been invoked from the Command Prompt, and have created a custom "MyCustomConsole" class which uses P/Invoke to Win32 to call AttachConsole(-1). At this point, I can read/write from the CLI as my program was a Console application. If I haven't received any extra arguments, then I can launch a Windows Form GUI as expected ("Application.Run(new Form1());").

The problem is that the code I end up invoking to perform blocking operations ("RunBlockingOperationsAsync()") is Async/Await to remain responsive, and when invoked via the GUI (through "Application.Run()"), works fine. If I try to call "RunBlockingOperationsAsync" without "Application.Run()", the program deadlocks or jumps to unexpected areas whilst debugging, effectively crashing.

I tried implementing a WindowsFormsSynchronizationContext, but that fails in the same manner. However, utilizing Stephen Toub's "AsyncPump.cs" solution corrects the problem (see below.)

There must be a built-in .NET framework piece for this, right? I can't believe Async/Await could be so thoroughly implemented without a default implementation for Console applications. My current understanding is that Async/Await utilization within a Console application without Stephen's "AsyncPump.cs" class (or similar) would not execute properly; effectively, this makes using Async/Await in a Console application unusable as-is by default.

It seems like Console applications should have an equivalent version of "Application.Run()", which initializes an appropriate SynchronizationContext (and whatever else might be necessary—maybe nothing right now.)

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


Resolution

The root of this problem is two-fold: First, a developer using Async/Await should understand that Async/Await's implementation can differ depending on SynchronizationContext; Stephen Toub does an excellent job explaining here. Understanding that a Console application does not have a specific SynchronizationContext by default, continuations are posted to the ThreadPool. If you debug a Console application, you would find that monitoring SynchronizationContext.Current is NULL.

Second, recognize that (for Windows Forms) Application.Run() sets up a Message Pump and a single-threaded SynchronizationContext. Monitoring SynchronizationContext.Current after Application.Run() would return a WindowsFormsSynchronizationContext object. Thanks to @noseratio, I've learned that instantiating a Windows Forms UserControl object will also instantiate and set SynchronizationContext.Current to use the new WindowsFormsSynchronizationContext, but only if it was NULL to begin with.

This explains my problem: The application I'm working on is a Windows Forms application, and when typically started, Application.Run() is used to invoke the Message Pump and also sets up a WindowsFormsSynchronizationContext. Async/Await works perfectly. However, when adding on support for CLI, I instantiated an object that derives from UserControl. As soon as I instantiate it, my formerly-NULL SynchronizationContext is now a WindowsFormsSynchronizationContext, and now Async/Await continuations are posted to it instead of the ThreadPool—what happens to continuations on the ThreadPool after a new SynchronizationContext is instantiated, I can't say. I experienced erratic program behavior, typically either "await Task.Delay()" calls hanging indefinitely, or control of my application (in the debugger) jumping around seemingly at random. Reportedly, setting (WindowsFormsSynchronizationContext.AutoInstall = false) should prevent automatically replacing a NULL SynchronizationContext with a WindowsFormsSynchronizationContext, but in my testing, it was still replaced (and Async/Await still broke.)

I did not test this with WPF, but I expect WPF would behave similarly (and/or developers would face a similar problem.)

There are multiple solutions:

  1. The best solution, in my opinion, is to not instantiate a Windows Forms UserControl (or WPF equivalent) when you're executing in CLI mode, if you can help it. Abstract work into it's own classes and leave UserControls (and their equivalents) to View abstractions if possible. This allows Async/Await to run on whatever Synchronization Context your application needs: If Windows Forms, a WindowsFormsSynchronizationContext. If WPF, a Dispatcher (?) SynchronizationContext. If a Console application, it runs on the ThreadPool instead of a SynchronizationContext.

  2. Explicitly set your own SynchronizationContext: @Stephen Toub's AsyncPump class; or @Stephen Cleary's AsyncContext class; or either of @TheodorZoulias's solutions worked (in my testing.) There may be good reason for using one of these solutions over #1, for example you may be working on a Console application, but have no choice but to instantiate a WinForms UserControl, or perhaps use a library that does so under-the-hood, unbeknownst to you. I would suggest monitoring SynchronizationContext.Current in various stages of an application if faced with this scenario.

解决方案

In the absence of synchronization context (or when the default SyncrhonizationContext is used), it's often possible for an await continuation to run synchronously, i.e., on the same thread where its antecedent task has ended. That can lead to obscure deadlocks, and it was one of the reasons TaskContinuationOptions.RunContinuationsAsynchronously was introduced in .NET Framework 4.6. For some more details and examples, check out this blog post: The danger of TaskCompletionSource class.

The fact that AsyncPump stops your code from hanging indicates you may have a similar situation somewhere inside mcc.Run(). As AsyncPump imposes true asynchrony for await continuations (albeit on the same thread), it reduces the chance for deadlocks.

That said, I'm not suggesting using AsyncPump or WindowsFormsSynchronizationContext as a workaround. Rather, you should try to find what exactly causes your code to hang (and where), and solve it locally, e.g. simply by wrapping the offending call with Task.Run.

One other issue I can spot in your code is that you don't wait or await the task returned by MainAsync. Because of that, at least for the console branch of your logic (especially without using AsyncPump), your program may be ending prematurely, depending on what's going in inside mcc.Run(), and you may be letting some exceptions go unobserved.

这篇关于调用Application.Run()或实例化WinForms UserControl对象时,在.NET Console中使用Async/Await中断应用程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆