在 C# 中从主线程以外的线程访问 COM 对象很慢

本文关键字:线程 访问 COM 对象 | 更新日期: 2023-09-27 18:34:36

我有一个专有的COM库,它返回一个整数数组(当然是它们自己的专有格式(。 当我从主 UI 线程访问此数组时,一切都很好并且运行迅速。 当我从另一个线程访问它时,访问速度非常慢。 下面是一些示例代码。

private void test() {
    ProprietaryLib.Integers ints = ProprietaryLib.GetInts();
    int x;
    for(int i = 0; i < 500; i++)
        for(int j = 0; j < ints.Count; j++)
            x = ints[j];
}
private void button1_Click(object sender, EventArgs e) {
    test();  // Very little time
    new System.Threading.Thread(() => test()).Start(); // Lots of time
}

为什么会这样? 有什么办法可以让我加快速度吗? 如果我使用多处理而不是多线程,那么我是否有希望获得良好的性能? (不过,听起来要复杂得多。

更新:

我对下面的答案感到满意,但想在这里添加一些数据以供参考(我自己和其他任何人的数据(。

如上所示,在新线程中创建和访问对象每次访问大约 12ns。 据推测,对象实际上是在主线程上创建的,速度慢是由于从那里封送数据。

如果在主线程上显式创建数据,但在标记为单线程单元的新线程中访问它,则访问时间甚至更慢,每次访问 15 ns。 我想.NET必须有一些额外的开销才能保持公寓的美观,尽管它让我担心我不知道开销是什么。 只有 2-3 ns 的差异,但不必太大。

如果在标记为 STA 的新线程上创建和访问对象,则每次访问的时间会以 .2ns 的速度消失。 但是这个新线程真的安全吗? 我认为这是另一个问题的问题。

在 C# 中从主线程以外的线程访问 COM 对象很慢

COM

对象具有线程相关性,它们可以告诉 COM 它们不是线程安全的。 注册表中有一个项,即"线程模型"项。 绝大多数人都这样做,要么指定"公寓",要么只是省略键。 它在 .NET 中不太明确,它使用 MSDN 告诉您类不是线程安全的,并且不会提醒您忘记阅读本文。 绝大多数 .NET 类不是线程安全的,与 COM coclass 没有什么不同。 与 .NET 不同,COM 确保以线程安全的方式调用它们。 通过自动封送对创建对象的线程的调用。

换句话说,没有并发性并且非常慢。

取得成功的唯一方法是创建自己的 Thread 并调用其 SetApartmentState(( 方法来切换到 STA,这是一个非线程安全的 COM 对象的快乐家园。 您还必须在该线程上创建 COM 对象。 而且您可能必须抽取消息循环以使其保持活动状态,这是 STA 的要求。 并且永远不要阻塞线程。 这些是使它成为一个非线程安全的类的快乐家园的事情,如果所有调用都在一个线程上进行,那么就不会出错。 您可以在此答案中找到此类线程的示例实现。

或者换句话说,当将线程与非线程安全的对象一起使用时,没有免费的午餐。 .NET 允许您通过忘记在需要时使用来射击您的脚,COM 使其自动。 这样单腿跳跃的程序员要少得多,但效率不高。

这可能取决于螺纹单元模型。如果您使用的是单线程单元模型 (STA(,则可能会遇到性能问题(如果数据大小足够大(。如果可以(如果您不使用另一个需要 STA 的 COM 对象(,您可以尝试将公寓模型更改为 MTA(多线程公寓模型(。

注意:WinForms 与 MTA 兼容,它始终检查单元模型是否是单线程的,因为它使用的某些 COM 对象(例如剪贴板和拖放(需要。我从未尝试过,但如果您不使用该功能,也许它可以工作。

从 MSDN:

由于对对象的调用不会以任何方式序列化,因此多线程对象并发可提供最高的性能,并充分利用多处理器硬件进行跨线程、跨进程和跨计算机调用。

其他参考资料
关于SO:你能解释一下STA和MTA吗?
MSDN: MTAThreadAttribute

尝试使用 Invoke() 在 UI 线程上执行 COM 调用:

private void button1_Click(object sender, EventArgs e) {
    ThreadPool.QueueUserWorkItem(delegate {
        this.Invoke((Action)(() => {
            test();
        }));
    });
}
在此

调用 Invoke() 之前和之后执行长时间运行操作的其余操作,以便仅在 UI 线程中运行快速 COM 调用。 此外,根据您正在做的事情,您可能会摆脱很多括号和其他线路噪音。