等待和的SynchronizationContext在非托管的应用程序托管的托管组件

等待和的SynchronizationContext在非托管的应用程序托管的托管组件

问题描述:

这似乎是在框架的实现中的错误 Application.DoEvents 中,我'已经报道此处。在UI线程恢复一个错误的同步环境可能会严重影响组件开发人员喜欢我。赏金的目的是吸引更多的关注这一问题,并奖励@MattSmith的答案帮助了追踪了下去。

This appears to be a bug in the Framework's implementation of Application.DoEvents, which I've reported here. Restoring a wrong synchronization context on a UI thread may seriously affect component developers like me. The goal of the bounty is to draw more attention to this problem and to reward @MattSmith whose answer helped tracking it down.

我负责一个.NET的WinForms 用户控件暴露的ActiveX为遗留的非托管的应用程序系成分,通过COM互操作。运行时的要求是.NET 4.0 + Microsoft.Bcl.Async。

I'm responsible for a .NET WinForms UserControl-based component exposed as ActiveX to a legacy unmanaged app, via COM interop. The runtime requirement is .NET 4.0 + Microsoft.Bcl.Async.

该组件被实例化,以及应用程序的主STA UI线程上使用。它的实现利用异步/计谋,因此预计,串行化同步上下文的实例已经安装在当前线程上(即 WindowsFormsSynchronizationContext )。

The component gets instantiated and used on the app's main STA UI thread. Its implementation utilizes async/await, so it expects that an instance of a serializing synchronization context has been installed on the current thread (i. e.,WindowsFormsSynchronizationContext).

通常, WindowsFormsSynchronizationContext 被设置为 Application.Run ,这就是一个管理应用程序的消息循环运行。当然,这是不是非托管主机应用程序的情况下,我有过这个没有控制权。当然,主机应用程序还是有自己传统的Windows消息循环,所以它不应该是一个问题,序列化计谋延续回调。

Usually, WindowsFormsSynchronizationContext gets set up by Application.Run, which is where the message loop of a managed app runs. Naturally, this is not the case for the unmanaged host app, and I have no control over this. Of course, the host app still has its own classic Windows message loop, so it should not be a problem to serialize await continuation callbacks.

然而,没有我来了这么远的解决方案是完美的,甚至正常工作。下面是一个人为的例子,其中由主机应用程序调用测试方法:

However, none of the solutions I've come up with so far is perfect, or even works properly. Here's an artificial example, where Test method is invoked by the host app:

Task testTask;

public void Test()
{
    this.testTask = TestAsync();
}

async Task TestAsync()
{
    Debug.Print("thread before await: {0}", Thread.CurrentThread.ManagedThreadId);

    var ctx1 = SynchronizationContext.Current;
    Debug.Print("ctx1: {0}", ctx1 != null? ctx1.GetType().Name: null);

    if (!(ctx1 is WindowsFormsSynchronizationContext))
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

    var ctx2 = SynchronizationContext.Current;
    Debug.Print("ctx2: {0}", ctx2.GetType().Name);

    await TaskEx.Delay(1000);

    Debug.WriteLine("thread after await: {0}", Thread.CurrentThread.ManagedThreadId);

    var ctx3 = SynchronizationContext.Current;
    Debug.Print("ctx3: {0}", ctx3 != null? ctx3.GetType().Name: null);

    Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
}

调试输出:


thread before await: 1
ctx1: SynchronizationContext
ctx2: WindowsFormsSynchronizationContext
thread after await: 1
ctx3: SynchronizationContext
ctx3 == ctx1: True, ctx3 == ctx2: False

虽然它继续在同一个线程中, WindowsFormsSynchronizationContext 情况下,我在在当前线程上安装等待 得到重置为默认的SynchronizationContext 后,因为某些原因。

Although it continues on the same thread, the WindowsFormsSynchronizationContext context I'm installing on the current thread before await gets reset to the default SynchronizationContext after it, for some reason.

为什么会被重置?我已经验证了我的部分是正在使用该应用程序的唯一.NET组件。该应用程序本身并调用的CoInitialize / OleInitialize 正常。

Why does it get reset? I've verified my component is the only .NET component being used by that app. The app itself does call CoInitialize/OleInitialize properly.

我也试过设置 WindowsFormsSynchronizationContext 在静态singleton对象的构造函数,所以它被安装的线程上时,我的管理组件被加载。这并没有帮助:当测试稍后在同一个线程中调用,上下文已被重置为默认的

I've also tried setting up WindowsFormsSynchronizationContext in the constructor of a static singleton object, so it gets installed on the thread when my managed assembly gets loaded. That didn't help: when Test is later invoked on the same thread, the context has been already reset to the default one.

我使用的是定制awaiter 考虑一>调度等待通过 control.BeginInvoke 回调我的控制>,因此上述看起来像等待TaskEx.Delay()。WithContext(控制)。这应该努力为自己的等待,只要主机的应用程序不断抽水的消息,但不为等待中的任何第三方组件的装配我可引用。

I'm considering using a custom awaiter to schedule await callbacks via control.BeginInvoke of my control, so the above would look like await TaskEx.Delay().WithContext(control). That should work for my own awaits, as long as the host app keeps pumping messages, but not for awaits inside any of the 3rd party assemblies my assembly may be referencing.

我还在研究这个。 任何关于如何保持对正确的线程关联等待在这种情况下将AP preciated。

I'm still researching this. Any ideas on how to keep the correct thread affinity for await in this scenario would be appreciated.

这将是一个有点长。首先,感谢马特·史密斯汉斯帕桑特作为你的想法,他们非常有帮助。

This is going to be a bit long. First of all, thanks Matt Smith and Hans Passant for your ideas, they have been very helpful.

该问题是由一个很好的老朋友引起的, Application.DoEvents ,虽然在一个新奇的方式。汉斯有优良的帖子为什么的DoEvents 是一个邪恶的。不幸的是,我是因为(在末尾更多关于它)所构成的传统的非托管主机应用程序的同步API的限制无法避免这种控制使用的DoEvents 我很清楚的的DoEvents 的现有影响,但在这里,我相信我们有一个新的:

The problem was caused by a good old friend, Application.DoEvents, although in a novelty way. Hans has an excellent post about why DoEvents is an evil. Unfortunately, I'm unable to avoid using DoEvents in this control, because of the synchronous API restrictions posed by the legacy unmanaged host app (more about it at the end). I'm well aware of the existing implications of DoEvents, but here I believe we have a new one:

在一个线程没有明确的WinForms消息循环(即,未进入任何线程 Application.Run Form.ShowDialog ),称 Application.DoEvents 将取代目前的同步上下文默认的SynchronizationContext ,提供 WindowsFormsSynchronizationContext.AutoInstall (这是所以默认)。

On a thread without explicit WinForms message loop (i.e., any thread which hasn't entered Application.Run or Form.ShowDialog), calling Application.DoEvents will replace the current synchronization context with the default SynchronizationContext, provided WindowsFormsSynchronizationContext.AutoInstall is true (which is so by default).

如果这是不是一个错误,那么它可能会严重影响一些组件开发一个非常不愉快的无证行为。

If it is not a bug, then it's a very unpleasant undocumented behavior which may seriously affect some component developers.

下面是一个简单的控制台应用程序STA再现问题。 注意如何 WindowsFormsSynchronizationContext 变(错误地)与的SynchronizationContext 的第一遍取代测试并没有在第二遍。

Here is a simple console STA app reproducing the problem. Note how WindowsFormsSynchronizationContext gets (incorrectly) replaced with SynchronizationContext in the first pass of Test and does not in the second pass.

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApplication
{
    class Program
    {
        [STAThreadAttribute]
        static void Main(string[] args)
        {
            Debug.Print("ApartmentState: {0}", Thread.CurrentThread.ApartmentState.ToString());
            Debug.Print("*** Test 1 ***");
            Test();
            SynchronizationContext.SetSynchronizationContext(null);
            WindowsFormsSynchronizationContext.AutoInstall = false;
            Debug.Print("*** Test 2 ***");
            Test();
        }

        static void DumpSyncContext(string id, string message, object ctx)
        {
            Debug.Print("{0}: {1} ({2})", id, ctx != null ? ctx.GetType().Name : "null", message);
        }

        static void Test()
        {
            Debug.Print("WindowsFormsSynchronizationContext.AutoInstall: {0}", WindowsFormsSynchronizationContext.AutoInstall);
            var ctx1 = SynchronizationContext.Current;
            DumpSyncContext("ctx1", "before setting up the context", ctx1);

            if (!(ctx1 is WindowsFormsSynchronizationContext))
                SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

            var ctx2 = SynchronizationContext.Current;
            DumpSyncContext("ctx2", "before Application.DoEvents", ctx2);

            Application.DoEvents();

            var ctx3 = SynchronizationContext.Current;
            DumpSyncContext("ctx3", "after Application.DoEvents", ctx3);

            Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
        }
    }
}

调试输出:


ApartmentState: STA
*** Test 1 ***
WindowsFormsSynchronizationContext.AutoInstall: True
ctx1: null (before setting up the context)
ctx2: WindowsFormsSynchronizationContext (before Application.DoEvents)
ctx3: SynchronizationContext (after Application.DoEvents)
ctx3 == ctx1: False, ctx3 == ctx2: False
*** Test 2 ***
WindowsFormsSynchronizationContext.AutoInstall: False
ctx1: null (before setting up the context)
ctx2: WindowsFormsSynchronizationContext (before Application.DoEvents)
ctx3: WindowsFormsSynchronizationContext (after Application.DoEvents)
ctx3 == ctx1: False, ctx3 == ctx2: True

花框架实施 Application.ThreadContext.RunMessageLoopInner WindowsFormsSynchronizationContext.InstalIifNeeded / 卸载明白到底为什么它发生。条件是该线程当前不执行应用程序消息循环,如上所述。从相关片 RunMessageLoopInner

It took some investigation of the Framework's implementation of Application.ThreadContext.RunMessageLoopInner and WindowsFormsSynchronizationContext.InstalIifNeeded/Uninstall to understand why exactly it happens. The condition is that the thread doesn't currently execute an Application message loop, as mentioned above. The relevant piece from RunMessageLoopInner:

if (this.messageLoopCount == 1)
{
    WindowsFormsSynchronizationContext.InstallIfNeeded();
}

然后在 WindowsFormsSynchronizationContext.InstallIfNeeded / 卸载对方法的的code不保存/恢复线程现有的同步上下文正确。在这一点上,我不知道它是否是一个错误或设计功能。

Then the code inside WindowsFormsSynchronizationContext.InstallIfNeeded/Uninstall pair of methods doesn't save/restore the thread's existing synchronization context correctly. At this point, I'm not sure if it's a bug or a design feature.

的解决办法是禁用 WindowsFormsSynchronizationContext.AutoInstall ,像这样简单:

The solution is to disable WindowsFormsSynchronizationContext.AutoInstall, as simple as this:

struct SyncContextSetup
{
    public SyncContextSetup(bool autoInstall)
    {
        WindowsFormsSynchronizationContext.AutoInstall = autoInstall;
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
    }
}

static readonly SyncContextSetup _syncContextSetup =
    new SyncContextSetup(autoInstall: false);

的几句话,为什么我用 Application.DoEvents 在这里的第一个地方。这是一个典型的异步到同步桥code在UI线程上运行,使用嵌套的消息循环。这是一个不好的做法,但在传统主机应用程序,希望所有的API来完成同步。原来的问题是这里描述。在以后的某个时候,我换成 CoWaitForMultipleHandles 与组合 Application.DoEvents / MsgWaitForMultipleObjects ,现在看起来是这样的:

A few words about why I use Application.DoEvents in the first place here. It's a typical asynchronous-to-synchronous bridge code running on the UI thread, using a nested message loop. This is a bad practice, but the legacy host app expects all APIs to complete synchronously. The original problem is described here. At some later point, I replaced CoWaitForMultipleHandles with a combination of Application.DoEvents/MsgWaitForMultipleObjects, which now looks like this:

最新版本的 WaitWithDoEvents 是的此处 [/ EDITED]

The most recent version of WaitWithDoEvents is here. [/EDITED]

当时的想法是调度使用.net标准机制的消息,而不是依赖于 CoWaitForMultipleHandles 这样做。这时候,我含蓄地引入同步方面的问题,由于所描述的行为的DoEvents

The idea was to dispatch messages using .NET standard mechanism, rather than relying upon CoWaitForMultipleHandles to do so. That's when I implicitly introduced the problem with the synchronization context, due to the described behavior of DoEvents.

继承应用程序正在使用现代技术重新编写,因此是控制。当前实现的目的是为现有的客户提供Windows XP中谁也不能升级超出我们控制的原因。

The legacy app is currently being rewritten using modern technologies, and so is the control. The current implementation is aimed for existing customers with Windows XP who cannot upgrade for reasons beyond our control.

最后,这里的自定义awaiter 我提到我的问题作为一个选项,以缓解这个问题的实现。这是一个有趣的经验和它的作品,但它不能被视为一个妥善的解决办法

Finally, here's the implementation of the custom awaiter which I mentioned in my question as an option to mitigate the problem. It was an interesting experience and it works, but it cannot be considered a proper solution.

/// <summary>
/// AwaitHelpers - custom awaiters
/// WithContext continues on the control's thread after await
/// E.g.: await TaskEx.Delay(1000).WithContext(this)
/// </summary>
public static class AwaitHelpers
{
    public static ContextAwaiter<T> WithContext<T>(this Task<T> task, Control control, bool alwaysAsync = false)
    {
        return new ContextAwaiter<T>(task, control, alwaysAsync);
    }

    // ContextAwaiter<T>
    public class ContextAwaiter<T> : INotifyCompletion
    {
        readonly Control _control;
        readonly TaskAwaiter<T> _awaiter;
        readonly bool _alwaysAsync;

        public ContextAwaiter(Task<T> task, Control control, bool alwaysAsync)
        {
            _awaiter = task.GetAwaiter();
            _control = control;
            _alwaysAsync = alwaysAsync;
        }

        public ContextAwaiter<T> GetAwaiter() { return this; }

        public bool IsCompleted { get { return !_alwaysAsync && _awaiter.IsCompleted; } }

        public void OnCompleted(Action continuation)
        {
            if (_alwaysAsync || _control.InvokeRequired)
            {
                Action<Action> callback = (c) => _awaiter.OnCompleted(c);
                _control.BeginInvoke(callback, continuation);
            }
            else
                _awaiter.OnCompleted(continuation);
        }

        public T GetResult()
        {
            return _awaiter.GetResult();
        }
    }
}