从 .Net 线程调用时 COM 调用挂起

Posted

技术标签:

【中文标题】从 .Net 线程调用时 COM 调用挂起【英文标题】:COM call hangs when called from .Net Thread 【发布时间】:2011-06-22 16:26:06 【问题描述】:

我有一个调用 COM 组件的 c# 静态函数。

当从一个普通的 WPF 应用程序调用这个函数时,它会正确返回。 代码可能如下所示:

var result = MyClass.StaticCall();
Debug.WriteLine("Hurrah!");

当我调用该代码时,将设置变量并按预期输出调试消息。

但是,如果我将同一个调用包装在一个线程中,它就永远不会返回。失败的代码可能如下所示:

var foo = new Thread(new ThreadStart(() =>
                               
                                   var result = MyClass.StaticCall();
                                   Debug.WriteLine("Hurrah!");
                               ));
foo.Start();

while (foo.IsAlive)


在这种情况下,StaticCall 不会返回,线程被无限期阻塞。

什么可能导致这种行为?

附加信息:

设置线程的appartment状态没有区别。 Visual Studio 输出窗口中的最后一条消息是 COM 互操作已加载的通知。 对 COM 的所有调用都在一个线程上隔离。

【问题讨论】:

代码after foo.Start() 是什么?这很重要。当你的主线程没有空闲时,死锁很常见。 @Hans 我已编辑问题以添加该详细信息。 【参考方案1】:

我似乎想起了一些关于 COM 需要在使用 COM 互操作的线程中运行的活动消息循环的事情。我不知道 .NET 的 COM 互操作实现的细节,但是在旧的 Win32 中,如果您在完成所有特殊初始化步骤后尝试在 COM 上进行进程间调用,它仍然会冻结在 COM 调用中,正如您所描述的那样。向后台线程添加一个简单的 peekmessage 循环,调用就会通过。也许您的 .NET 代码需要做类似的事情?

背景:COM 进程间通信通过 COM 创建的窗口句柄依赖于 SendMessage。如果该窗口句柄是在您的主线程上创建的,那么发送到该窗口句柄的消息将由您的主线程的消息循环处理,一切都很好。如果该窗口句柄是在后台线程上创建的,则发送到该窗口句柄的消息只能由该线程中运行的消息循环检索。

试试这个:在后台线程上进行 COM 调用之前,先从主线程进行一次 COM 调用。这将强制 COM 在您有消息循环的主线程上进行初始化。看看这是否会解除对后台 COM 调用的阻止。只是一个想法。

【讨论】:

我已尝试在创建线程之前在主线程上添加对 StaticCall 的调用,如问题中所述。主线程上的 StaticCall 返回正常,但线程上的调用仍然阻塞。我将研究您关于在后台线程上抽取消息队列的想法。谢谢【参考方案2】:
foo.Start();

while (foo.IsAlive)


是的,这是保证的死锁。 COM 对象通常具有线程亲和性。他们可以告诉 COM 他们不是线程安全的。绝大多数不是,就像 .NET 类一样。 COM 然后负责以线程安全的方式调用它们。与 .NET 非常不同的是,它完全由您来提供线程安全。

您在主线程上创建了 COM 对象。所以COM必须在主线程上调用COM对象,以保证安全。它不能那样做,你的主线程很忙。正在循环 foo.IsAlive 属性。在您的主线程空闲之前,在工作线程上进行的任何调用都无法完成。在工作线程完成之前,您的主线程不能空闲。死锁城市。

一个修复是这样的:

foo.Start();
foo.Join();

虽然 Thread.Join() 也是一个阻塞调用,但它确实有效。 CLR 实现它,它知道阻塞 STA 线程是非法的。它泵送一个消息循环,允许编组 COM 调用并允许线程完成。

嗯,这可能不是你想要做的,你正在那个循环中做一些事情。您唯一可以做的另一件事是调用 Application.DoEvents(),这也将泵送消息循环。这很危险,您必须禁用用户界面,以便用户无法关闭主窗口并且无法再次启动线程。

这就是没有免费午餐的原则,你不能神奇地让不是线程安全的代码支持线程。没有并发,COM 对象方法仍然在你的主线程上运行。如果他们需要很长时间,他们仍然会冻结 UI。

这会导致其他解决方法,而是在您的工作线程上创建 COM 对象。它必须是 STA,并且通常需要泵送消息循环。

【讨论】:

因此,用 foo.Join() 替换 while 循环解决了我的示例中的问题,但我注意到,像 Jalal 一样,您已经提到在工作线程上创建 COM 对象。我的印象是我已经在这样做了。鉴于 StaticCall 创建 COM 对象并调用它,是什么让您认为 COM 对象是在主线程上创建的?也许我遗漏了一个基本点? 我没有在 sn-p 中看到 new 语句。代码不够,不知道为什么COM需要消息循环来编组。显然它确实如此。使用Debug + Windows + Threads在线程之间切换,看看worker被阻塞的地方。【参考方案3】:

如果对该方法的正常调用(不启动新线程)没有任何问题,那么这主要是因为您没有从创建 com 对象的同一线程调用该方法。要创建自定义调用机制,请参阅 this。

【讨论】:

"所有对 COM 的调用都隔离在一个线程上。"我的意思是 COM 对象是在同一个线程上创建和调用的。

以上是关于从 .Net 线程调用时 COM 调用挂起的主要内容,如果未能解决你的问题,请参考以下文章

为啥在不同线程中调用 asyncio subprocess.communicate 会挂起?

BackgroundWorker 使用 COM 对象导致 UI 线程挂起

远程主机终止后,Recv() 调用挂起

ASP.NET 应用程序在调用后挂起

为啥挂起的意图在从服务的通知中调用时不会启动活动

如何正确终止 dll 中的挂起线程?