在C#中,从主线程以外的线程访问COM对象速度较慢

4

我有一个专有的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纳秒。我猜.NET必须有一些额外的开销来保持公寓的美观,虽然我不知道那个开销是什么,只有2-3ns的差异,但它也不需要太多。
如果你在标记为STA的新线程上创建和访问对象,则时间会以每次0.2ns的速度消失。但是这个新线程真的安全吗?这是另一个问题的问题。

数组大小足够吗?默认的 STA 意味着线程间的封送。 - Adriano Repetti
3个回答

8
COM对象具有线程亲和性,它们可以告诉COM它们不是线程安全的。通过注册表中的“ThreadingModel”键,大多数对象都会指定“Apartment”或省略该键来实现这一点。在.NET中,这种情况不太明确,它使用MSDN告诉你哪些类不是线程安全的,但不会提醒你是否忘记了阅读文章。绝大多数.NET类与COM coclass一样不是线程安全的。不同的是,COM确保以线程安全的方式调用它们,通过自动将调用代理到创建对象的线程。

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

想要成功,唯一的方法是创建自己的线程,并调用其SetApartmentState()方法切换到STA,一个适合不是线程安全的COM对象的理想环境。还必须在该线程上创建COM对象。并可能需要pump a message loop来保持其活动状态,符合STA的要求。而且永远不要阻止该线程。这些是让不是线程安全的类成为快乐之家的因素,如果所有调用都在一个线程上进行,则不会出错。您可以在此答案中找到这样一个线程的示例实现。

换句话说,使用不是线程安全的对象时,没有免费的午餐。 .NET让您在需要时忘记使用lock而自己射脚,而COM则可以自动完成它。这样做能减少很多程序员跳起单脚的情况,但效率不高。


那么,如果我编写一个单线程控制台程序,并且它使用STA COM对象,并且它进行了大量处理,因此没有在泵送消息循环,而且也许它正在等待来自系统其他地方的消息,因此偶尔会阻塞,这样可以吗? - user12861
如果您有时间,请查看此链接中的我的下一个问题,并让我知道我所有的误解:http://stackoverflow.com/questions/10654098/is-sta-message-loop-required-in-this-case - user12861

2
这可能取决于线程公寓模型。如果您使用单线程公寓模型(STA),则在处理大量数据时可能会遇到性能问题。如果可以的话(如果您没有使用需要STA的其他COM对象),可以尝试将公寓模型更改为MTA(多线程公寓模型)。
注意:WinForms不兼容MTA,它始终检查公寓模型是否为单线程,因为它所使用的某些COM对象(例如剪贴板和拖放)需要此模型。如果您不使用这些功能,则可能会起作用。(我从未尝试过)
来自MSDN的说明:
因为对象调用没有以任何方式序列化,所以多线程对象并发提供了最高的性能,并且最好地利用了跨线程、跨进程和跨计算机调用的多处理器硬件。
附加参考资料
在SO上:你能解释一下STA和MTA吗?
MSDN:MTAThreadAttribute

它是如何确定哪个线程需要进行封送的?也就是说,为什么在 UI 线程上速度很快(暗示没有封送),但在其他线程上却很慢,即使对象是在该线程上创建的? - user12861
我不知道它内部发生了什么,但我猜测它总是进行编组(根据您的结果,但没有其他证据)。 - Adriano Repetti

1
尝试使用Invoke()在UI线程上执行COM调用:
private void button1_Click(object sender, EventArgs e) {
    ThreadPool.QueueUserWorkItem(delegate {
        this.Invoke((Action)(() => {
            test();
        }));
    });
}

在调用Invoke()之前和之后执行您的长时间运行操作,以便只有快速的COM调用在UI线程中运行。此外,根据您的操作,您可能可以消除许多括号和其他行噪声。

一个好的建议,但对我来说行不通。这是一个长时间运行的进程,我想让它远离UI线程。 - user12861
那是一个很有趣的想法。我可能会尝试一下。不确定你的语法是否完全正确,但它确实可以让代码快速运行,稍微修正一下语法就好了。 - user12861
哦,顺便说一下,真正慢的不是 GetInts() 函数本身,而是访问这些整数的过程。但我可以将它们复制到主线程中的简单 List<int> 中,然后稍后再使用它们...现在正在研究这个问题。 - user12861
通过该调用,您仍然在主线程中执行test()(将其加入到池中,然后再次加入到主线程)。 - Adriano Repetti
此外,将函数分成两个线程的想法,如果可行,是很好的,听起来也不错! - Adriano Repetti
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接