因为 STAThread 属性是在我的 main 方法上设置的
是的,这是 VB.NET 继承自 VB6 的一个令人遗憾的做法。COM(VB6 的原始基础以及您在 Web 服务中使用的内容)的一个重要目标是隐藏线程的复杂性并自动处理线程不安全的代码,而客户端程序员不必对此有所了解。COM 对象告诉 COM 运行时它支持哪种线程。到目前为止,最常见的选择是“Apartment”,这是一个令人困惑的词,意味着它不是线程安全的。
COM 通过将 COM 方法的调用从工作线程自动封送到创建 COM 对象的线程来解决线程安全问题。从而保证 COM 对象的线程安全。.NET 中的等价物是 Dispatcher.Invoke() 或 Control.Invoke()。您必须在 .NET 程序中显式调用以保持线程不安全用户界面正常工作的方法,对于 COM 对象,它完全自动完成。
这种封送处理非常昂贵,它不可避免地涉及两个线程上下文切换以及序列化方法参数的开销,至少需要数万个 CPU 周期。
线程可以告诉 COM,它是线程不安全 COM 对象的友好归宿,并且会处理编组要求,它将自己标记为单线程单元。STA。它对 COM 方法的任何调用都不必进行封送处理并全速运行。如果从工作线程进行调用,则 STA 线程负责实际进行调用。
然而,STA 线程必须遵守两个非常重要的规则。违反这些规则之一会导致很难诊断运行时故障。如果您违反这些规则,就会发生死锁,就像您在终结器线程中观察到的那样。他们是:
STA 线程必须泵送消息循环。.NET 程序中的 Application.Run() 等价物。正是消息循环实现了生产者-消费者问题的通用解决方案。需要能够将一个线程的调用编组到特定的其他线程。如果它没有抽水,那么在工作线程上进行的调用将无法完成并且会死锁。
不允许 STA 线程阻塞。阻塞大大增加了死锁的几率,阻塞的线程不会发送消息。.NET 程序中的问题较小,CLR 对在 WaitHandle.WaitOne() 和 Thread.Join() 之类的调用上抽水有很大的支持。
有时,COM 组件本身会做出关于由 STA 线程拥有的硬性假设。并在内部使用 PostMessage(),通常用于引发事件。因此,即使您实际上从未在工作线程上进行过任何调用,该组件仍然会发生故障。WebBrowser 是最臭名昭著的例子,它的 DocumentCompleted 事件不会在线程不启动时触发。
您的 Web 服务无疑违反了第一条。您只能在 Winforms 或 WPF 应用程序中自动获得消息循环。是的,终结器线程中毒了,因为它对 COM 对象的最终释放调用必须被编组以保持对象线程安全。死锁是不可避免的结果,因为 STA 线程没有抽水。一个很难诊断的棘手问题,您得到的唯一提示是程序的内存使用量激增。
通过将线程标记为 MTA,您明确承诺不会为单元线程 COM 服务器提供安全的家。COM现在被迫处理hard case,它必须自己创建一个线程来提供安全性。那个线程总是泵。虽然这可以解决您的 Web 服务器的问题,但应该注意这不是万能的。那些额外的线程不是免费的,而且调用总是被编组的,所以总是很慢。获得太多这些帮助线程是一个很难诊断的棘手问题,您得到的唯一提示是程序的内存使用量爆炸式增长:)
自动线程安全是一个非常好的特性。它在 99% 的时间内都可以正常工作,没有任何麻烦。然而,摆脱 1% 的故障模式是一个非常令人头疼的问题。归根结底,它归结为普遍真理,线程复杂且容易出错。一种方法是不要把它留给 COM,而是自己亲自动手。这篇文章中的代码可能对此有所帮助。