这会有点长。首先,感谢Matt Smith和Hans Passant的想法,他们非常有帮助。
这个问题是由一位好老朋友引起的Application.DoEvents
,虽然方式很新颖。汉斯有一篇关于为什么是邪恶的优秀帖子。DoEvents
不幸的是,我无法避免DoEvents
在此控件中使用,因为旧版非托管主机应用程序带来了同步 API 限制(最后会详细介绍)。我很清楚 的现有含义DoEvents
,但在这里我相信我们有一个新含义:
在没有显式 WinForms 消息循环的线程上(即,任何未进入Application.Run
or的线程Form.ShowDialog
),调用Application.DoEvents
将用默认值替换当前同步上下文SynchronizationContext
,前提WindowsFormsSynchronizationContext.AutoInstall
是 is true
(默认情况下是这样)。
如果它不是一个错误,那么它是一种非常令人不快的未记录行为,可能会严重影响一些组件开发人员。
这是一个重现问题的简单控制台 STA 应用程序。请注意在第一遍中如何(WindowsFormsSynchronizationContext
错误地)替换为,而在第二遍中如何不替换。SynchronizationContext
Test
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);
}
}
}
调试输出:
公寓州:STA
*** 测试 1 ***
WindowsFormsSynchronizationContext.AutoInstall:真
ctx1: null(在设置上下文之前)
ctx2:WindowsFormsSynchronizationContext(在 Application.DoEvents 之前)
ctx3:SynchronizationContext(在 Application.DoEvents 之后)
ctx3 == ctx1:假,ctx3 == ctx2:假
*** 测试 2 ***
WindowsFormsSynchronizationContext.AutoInstall:假
ctx1: null(在设置上下文之前)
ctx2:WindowsFormsSynchronizationContext(在 Application.DoEvents 之前)
ctx3:WindowsFormsSynchronizationContext(在 Application.DoEvents 之后)
ctx3 == ctx1:假,ctx3 == ctx2:真
对框架的实现Application.ThreadContext.RunMessageLoopInner
和WindowsFormsSynchronizationContext.InstalIifNeeded
/进行了一些调查,Uninstall
以了解它发生的确切原因。条件是线程当前没有执行Application
消息循环,如上所述。相关文章来自RunMessageLoopInner
:
if (this.messageLoopCount == 1)
{
WindowsFormsSynchronizationContext.InstallIfNeeded();
}
然后WindowsFormsSynchronizationContext.InstallIfNeeded
/Uninstall
方法对中的代码无法正确保存/恢复线程的现有同步上下文。在这一点上,我不确定这是错误还是设计功能。
解决方案是禁用WindowsFormsSynchronizationContext.AutoInstall
,就像这样简单:
struct SyncContextSetup
{
public SyncContextSetup(bool autoInstall)
{
WindowsFormsSynchronizationContext.AutoInstall = autoInstall;
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}
}
static readonly SyncContextSetup _syncContextSetup =
new SyncContextSetup(autoInstall: false);
关于我为什么Application.DoEvents
首先在这里使用的几句话。它是在 UI 线程上运行的典型异步到同步桥代码,使用嵌套的消息循环。这是一种不好的做法,但旧版主机应用程序希望所有 API 同步完成。原始问题在此处描述。稍后,我用/CoWaitForMultipleHandles
的组合替换,现在看起来像这样:Application.DoEvents
MsgWaitForMultipleObjects
[已编辑]的最新版本WaitWithDoEvents
在这里。[/编辑]
这个想法是使用 .NET 标准机制来调度消息,而不是依赖于CoWaitForMultipleHandles
这样做。那时我隐含地介绍了同步上下文的问题,因为DoEvents
.
旧版应用程序目前正在使用现代技术进行重写,控件也是如此。当前的实施针对因我们无法控制的原因而无法升级的现有 Windows XP 客户。
最后,这是我在问题中提到的自定义等待器的实现,作为缓解问题的一种选择。这是一次有趣的经历,并且有效,但不能认为是适当的解决方案。
/// <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();
}
}
}