【发布时间】:2013-11-01 08:14:18
【问题描述】:
[EDITED] This appears to be a bug 在框架的Application.DoEvents 实现中,我已经报告了here。在 UI 线程上恢复错误的同步上下文可能会严重影响像我这样的组件开发人员。赏金的目的是引起对这个问题的更多关注,并奖励@MattSmith,他的回答帮助追踪了它。
我负责一个基于 .NET WinForms UserControl 的组件作为 ActiveX 通过 COM 互操作向旧版非托管应用程序公开。运行时要求是 .NET 4.0 + Microsoft.Bcl.Async。
组件被实例化并在应用的主 STA UI 线程上使用。它的实现使用了async/await,因此它期望一个序列化同步上下文的实例已经安装在当前线程上(即WindowsFormsSynchronizationContext)。
通常,WindowsFormsSynchronizationContext 由 Application.Run 设置,这是托管应用程序的消息循环运行的地方。当然,非托管主机应用程序并非如此,我无法控制这一点。当然宿主应用还是有自己经典的Windows消息循环,所以序列化await延续回调应该不是问题。
但是,到目前为止,我提出的解决方案都不是完美的,甚至都不能正常工作。这是一个人工示例,其中Test 方法由宿主应用程序调用:
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);
}
调试输出:
等待之前的线程:1 ctx1:同步上下文 ctx2:WindowsFormsSynchronizationContext 等待后的线程:1 ctx3:同步上下文 ctx3 == ctx1:真,ctx3 == ctx2:假虽然它在同一个线程上继续,但由于某种原因,我在await 在它之后重置为默认的SynchronizationContext 之前在当前线程上安装的WindowsFormsSynchronizationContext 上下文。
为什么会重置?我已经确认我的组件是该应用程序使用的唯一 .NET 组件。该应用程序本身确实正确调用了CoInitialize/OleInitialize。
我也尝试过在静态单例对象的构造函数中设置WindowsFormsSynchronizationContext,以便在加载我的托管程序集时将其安装在线程上。这没有帮助:当稍后在同一个线程上调用 Test 时,上下文已经重置为默认值。
我正在考虑使用custom awaiter 通过我的控件的control.BeginInvoke 安排await 回调,所以上面看起来像await TaskEx.Delay().WithContext(control)。这应该适用于我自己的awaits,只要主机应用程序不断发送消息,但不适用于我的程序集可能引用的任何第 3 方程序集内的awaits。
我还在研究这个。 任何关于如何在这种情况下为await 保持正确线程关联性的想法将不胜感激。
【问题讨论】:
-
很难猜到这一点。奇怪的细节是为什么 SynchronizationContext.Current 在代码开始运行时不为空。我们看不到代码是如何从非托管程序开始的。您可能正在与 Thread.ExecutionContext 作斗争,它会复制 SynchronizationContext 并恢复它。
-
@HansPassant,这种行为可以成为互操作层的一部分吗?我将尝试创建一个简单的复制案例。
-
嗯,当然。当此代码从非托管程序的 UI 线程以外的线程激活时,不要指望这会起作用。泵送消息循环并调用 CoInitializeEx() 并指定 COINIT_APARTMENTTHREADED 的那个。
-
@HansPassant,是的,非托管应用的两个条件都已与作者进行了专门验证。
-
不要相信他的话,验证一下。启用非托管调试并在代码上设置断点。查看调用堆栈并确认您在调用堆栈上看到 KiUserCallbackDispatch 和 InternalCallWinProc。并使用 Thread.CurrentThread.GetApartmentState() 并检查您是否获得了 STA。
标签: c# .net asynchronous async-await com-interop