【问题标题】:How to improve performance of background task in WPF?如何提高 WPF 中后台任务的性能?
【发布时间】:2013-02-25 21:42:34
【问题描述】:

我正在构建一个 WPF 应用程序,它将在后台执行一些繁重的工作。问题是当我在单元测试中运行任务时,通常需要大约 6~7 秒才能运行。但是当我在 WPF 应用程序中使用 TPL 运行它时,它需要 12 秒到 30 秒才能运行。有没有办法加快这件事。我正在调用 LogParser 的 COM api 来完成真正的工作。

更新: 我调用 Log Parser API 的代码如下所示

var thread = new Thread(() =>
            {
                var logQuery = new LogQueryClassClass();
                var inputFormat = new COMEventLogInputContextClassClass
                {
                    direction = "FW",
                    fullText = true,
                    resolveSIDs = false,
                    formatMessage = true,
                    formatMsg = true,
                    msgErrorMode = "MSG",
                    fullEventCode = false,
                    stringsSep = "|",
                    iCheckpoint = string.Empty,
                    binaryFormat = "HEX"
                };
                try
                {
                    Debug.AutoFlush = true;
                    var watch = Stopwatch.StartNew();
                    var recordset = logQuery.Execute(query, inputFormat);
                    watch.Stop();

                    watch = Stopwatch.StartNew();
                    while (!recordset.atEnd())
                    {
                        var record = recordset.getRecord();
                        recordProcessor(record);
                        recordset.moveNext();
                    }
                    recordset.close();
                    watch.Stop();
                }
                catch
                {
                }
                finally
                {
                    if (logQuery != null)
                    {
                        Marshal.ReleaseComObject(logQuery);
                        GC.SuppressFinalize(logQuery);
                        logQuery = null;
                    }
                }
            });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

现在的事情是有了这个改变,我可以看到调试模式大约有 3 - 4 秒的改进,但当我按下 Ctrl + F5 来运行它时却没有,这超出了我的想象。怎么来的??

【问题讨论】:

  • 如果是单元测试,能不能不模拟COM API或者里面用到的对象?
  • 不是用于单元测试,问题是在单元测试中,性能很好,但在实际应用中却没有。
  • cpu使用情况如何?有多少个核心?
  • 您是在任务中还是在主 (GUI) 线程中创建 COM 对象?主 GUI 线程是 STA,因此如果创建了 COM 对象,则来自您的任务的所有 COM 调用都需要编组回主线程。您的测试可能正在运行 MTA,因此不需要编组
  • @adrianm,不,我实际上是在运行该任务的线程中创建 COM 对象。所以这不应该是这样的。

标签: c# wpf com task-parallel-library


【解决方案1】:

这里的问题是您使用的 COM 对象只能在 STA 线程上运行。已经有几个人提出了这个建议,但我决定检查一下,只是为了确定。我安装了 LogParser SDK,下面是它为与 MSUtil.LogQuery ProgID 关联的 CLSID 放入注册表的内容:

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{8CFEBA94-3FC2-45CA-B9A5-9EDACF704F66}]
@="LogQuery"
"AppID"="{3040E2D1-C692-4081-91BB-75F08FEE0EF6}"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{8CFEBA94-3FC2-45CA-B9A5-9EDACF704F66}\InprocServer32]
@="C:\\Program Files (x86)\\Log Parser 2.2\\LogParser.dll"
"ThreadingModel"="Apartment"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{8CFEBA94-3FC2-45CA-B9A5-9EDACF704F66}\ProgID]
@="MSUtil.LogQuery.1"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{8CFEBA94-3FC2-45CA-B9A5-9EDACF704F66}\VersionIndependentProgID]
@="MSUtil.LogQuery"

关键是"ThreadingModel"="Apartment"。这个 COM 类声明它只能在 STA 线程上运行。

TPL 和 BackgroundWorker 都使用 MTA 线程。这样做的结果是,当您从 TPL 任务或 BackgroundWorker 使用 LogParser 时,COM 运行时会检测到您在错误类型的线程上,并且会找到或创建一个 STA 来托管该对象。 (在这种特殊情况下,它将使用所谓的“主机 STA”,这是 COM 专门为此目的创建的一个线程。在某些情况下,它会使用您的主 UI 线程,但这里不是这种情况。 )

COM 然后自动将来自您的工作线程的任何调用编组到该 STA 线程。它通过 Windows 消息队列执行此操作,因此对于您执行的每个方法(请记住,属性访问器只是变相的方法,因此这也适用于属性使用),您的工作线程将向该 STA 线程发送一条消息,该 STA然后,线程的消息泵必须选择该消息并将其分派,此时 COM 运行时将为您调用 LogParser 上的方法。

如果您的 API 涉及大量调用,这会很慢。

顺便说一句,这既不是 WPF 也不是 Windows 窗体问题。这完全与使用来自非 STA 线程的基于 STA 的 COM 对象有关。如果您在其中使用非 STA 线程,您也可以使用控制台应用程序重现完全相同的问题。而且这个问题并不特定于 TPL 或 BackgroundWorker - 它会影响任何使用线程池的东西,因为线程池线程都使用 MTA,而不是 STA。

解决方案是使用 STA 线程。最好的方法是创建一个专用线程。使用 System.Threading 命名空间中的 Thread 类来启动您自己的线程。在启动它之前调用它的SetApartmentState 方法。确保从 LogParser API 创建对象实例的代码正在该线程上运行,并确保您只使用该线程中的那些对象。这应该可以解决您的性能问题。

于 2013 年 2 月 21 日编辑澄清:

请注意,仅仅确保您使用来自 STA 线程的 COM 对象是不够的。您必须使用 if 来自 您创建它的同一 STA 线程。基本上,拥有 STA 模型的全部原因是使 COM 组件能够使用单线程模型。它使他们能够假设发生在他们身上的所有事情都发生在一个线程上。如果您编写使用来自多个线程的 STA 线程的多线程 .NET 代码,它将在幕后确保 COM 对象得到它想要的,这意味着所有访问都将通过它所属的线程。

这意味着,如果您从其主 STA 线程之外的某个其他线程调用它,那么即使该其他线程也恰好是 STA 线程,您仍将支付跨线程的代价。

于 2013 年 2 月 25 日编辑添加:

(不确定这是否与此特定问题相关,但其他通过搜索登陆此问题的人很可能会感兴趣。)将工作转移到单独的工作线程的一个缺点是,如果您想更新以任何方式处理这些记录的结果,你现在在错误的线程上。如果您使用数据绑定 INotifyPropertyChanged,WPF 将自动为您处理跨线程更改通知,但这可能会对性能产生重大影响。如果您需要在后台线程上做大量工作,但该工作最终需要更新 UI,您可能需要采取措施批量更新这些更新。这并非完全无关紧要 - 请参阅从此处开始的系列博客文章:http://www.interact-sw.co.uk/iangblog/2013/02/14/wpf-async-too-fast

【讨论】:

  • 或使用来自 TPL Extras 的 StaTaskScheduler。 blogs.msdn.com/b/pfxteam/archive/2010/04/07/9990421.aspx
  • 我用了这两种方法,使用 StaTaskScheduler 确保我的后台线程是 STA,并且还启动了一个新的 STA 线程来调用 LogParser COM API,但是在我调试时它只有明显的改进代码。当我运行它时,没有任何改进,奇怪。
  • 您是否正在采取措施确保您始终使用 same STA 线程?如果您的应用程序中有多个 STA 线程(如果您使用 StaTaskScheduler 指定大于 1 的并发级别就会出现这种情况,如果同时使用 TPL 和 new Thread 方法也会出现这种情况,或者如果您显式创建多个线程),只有在您碰巧在创建它的同一个 STA 线程上使用 COM 对象时,它才会快速运行。
  • @IanGriffiths,我确实将 2 指定为StaTaskScheduler 的并发级别,并且我还使用new Thread 创建了一个 STA 线程来执行 COM 调用。所以我想我应该做的是指定1作为并发级别并且不要使用new Thread来调用COM。我会试试这个。但是您的分析仍然没有回答为什么在调试模式下我们获得了 3 秒的增益。这个问题确实让我很困惑。
  • 实际上...查看您对原始问题的更新,看起来您已经从图片中完全删除了 TPL,而您只是使用了一个专用线程?所以我的调试器建议不成立。我刚刚在一个简单的测试查询中尝试了您的代码:SELECT TOP 5000 SourceName, EventID, Message FROM System,无论我是在主线程上运行它,还是使用您的线程代码,它都需要 1.14 秒,它在调试器内外的行为相同。简而言之,我无法重现您所描述的内容,而我正在使用您的代码。一定有遗漏的细节。
【解决方案2】:

COM 为 IPC 使用消息队列。我不清楚是什么决定了 which 消息队列,但我怀疑它是 shell 消息队列,因为 Delphi 调试器和 Outlook 曾经互相玩得很开心。我未经证实的假设是,进程外的 COM 服务器可能会因 else 停止 shell 消息队列的东西而停止。 Windows 有超时来防止这种事情完全锁定系统,但它可能会导致受影响的进程大幅减速。我的解决方案是避免使用 COM。您可以通过注释掉实际使用 COM 的部分并计时该过程来检查这一点。

【讨论】:

  • 问题是在一个winform应用程序中,性能与单元测试是内联的。而且我必须使用 COM,因为我知道 LogParser 没有其他可用的 API。
  • 在您的 Windows 窗体应用程序中,您是否也在使用 TPL?我正在尝试确定此问题是否特定于您使用 WPF 的事实,或者它实际上是否与使用 TPL 与您在 Windows 窗体应用程序中所做的任何事情有关。 (根据您的说法,最可能的罪魁祸首是 COM 公寓模型不匹配,但我还没有足够的信息来确定。)
  • @IanGriffiths,我尝试使用BackgroundWorker,这是演示Winform应用程序使用的,性能完全没有变化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-23
  • 1970-01-01
  • 2010-11-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多