【问题标题】:Reading output from console application and WPF plotting async从控制台应用程序读取输出和 WPF 绘图异步
【发布时间】:2011-06-30 13:19:57
【问题描述】:

我有一个控制台应用程序,它每 1 秒输出大约 160 行信息。

数据输出是可用于在图表上绘制的点。

在我的 WPF 应用程序中,我已经成功连接了这个,并且正在绘制控制台应用程序输出的数据,但是,在大约 500 个左右的数据点之后,我发现应用程序和 UI 线程锁定显着变慢.

我认为这是由于我正在使用的异步操作:

BackgroundWorker worker = new BackgroundWorker();

worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
    _process = new Process();
    _process.StartInfo.FileName = "consoleApp.exe";
    _process.StartInfo.UseShellExecute = false;
    _process.StartInfo.RedirectStandardOutput = true;
    _process.StartInfo.CreateNoWindow = true;
    _process.EnableRaisingEvents = true;
    _process.OutputDataReceived += new DataReceivedEventHandler(SortOutputHandler);
    _process.Start();
    _process.BeginOutputReadLine();
    _watch.Start();
};
worker.RunWorkerAsync();

以及负责解析和绘制数据的处理程序:

private void SortOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
    if (!String.IsNullOrEmpty(outLine.Data))
    {
            var xGroup = Regex.Match(outLine.Data, "x: ?([-0-9]*)").Groups[1];
            int x = int.Parse(xGroup.Value);

            var yGroup = Regex.Match(outLine.Data, "y: ?([-0-9]*)").Groups[1];
            int y = int.Parse(yGroup.Value);

            var zGroup = Regex.Match(outLine.Data, "z: ?([-0-9]*)").Groups[1];
            int z = int.Parse(zGroup.Value);


            Reading reading = new Reading()
            {
                Time = _watch.Elapsed.TotalMilliseconds,
                X = x,
                Y = y,
                Z = z
            };


            Dispatcher.Invoke(new Action(() =>
            {
                _readings.Enqueue(reading);
                _dataPointsCount++;

            }), System.Windows.Threading.DispatcherPriority.Normal);                    
    }
}

_readingsthis answer 中定义的自定义ObservableQueue<Queue>。我已经对其进行了修改,以便一次只能有 50 个项目在队列中。因此,如果正在添加新项目并且队列计数 >= 50,则在 Enqueue() 之前调用 Dequeue()

有什么方法可以提高性能,还是因为控制台应用程序的输出量太大而注定要失败?

【问题讨论】:

  • 您是否从主应用程序线程启动了进程?如果是这样,这可以解释 UI 锁定。
  • @Chris - 不,我正在为该过程使用后台工作人员。我用更多代码更新了我的问题。
  • 感谢您解决这个问题。至少我们知道去别处看看;)
  • 那么假设您一次只绘制 50 个点是否正确?
  • 是的,队列是一个可观察的集合,当且仅当当前的点数>= 50 时,它才会在入队之前出队

标签: multithreading wpftoolkit


【解决方案1】:

您可以简单地将这些点存储在一个列表中,并仅在您拥有例如达到 160 点,因此您不会创建太多更新消息。目前,您每 6 毫秒产生一个窗口消息,这实在是太多了。当您更新 UI 时,例如每一秒或每 160 点,事情都会变得更加顺畅。如果通知仍然太多,您需要查看如何在使用 160 个数据点更新 UI 并在之后恢复绘制时暂停重绘控件,以免出现严重闪烁。

List<Reading> _Readings = new List<Reading>();
DateTime _LastUpdateTime = DateTime.Now;
TimeSpan _UpdateInterval = new TimeSpan(0,0,0,0,1*1000); // Update every 1 second

private void SortOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
    if (!String.IsNullOrEmpty(outLine.Data))
    {
            var xGroup = Regex.Match(outLine.Data, "x: ?([-0-9]*)").Groups[1];
            int x = int.Parse(xGroup.Value);

            var yGroup = Regex.Match(outLine.Data, "y: ?([-0-9]*)").Groups[1];
            int y = int.Parse(yGroup.Value);

            var zGroup = Regex.Match(outLine.Data, "z: ?([-0-9]*)").Groups[1];
            int z = int.Parse(zGroup.Value);


            Reading reading = new Reading()
            {
                Time = _watch.Elapsed.TotalMilliseconds,
                X = x,
                Y = y,
                Z = z
            };

            // create a batch of readings until it is time to send it to the UI
            // via ONE window message and not hundreds per second. 
            _Readings.Add(reading);

            DateTime current = DateTime.Now;
            if( current -_LastUpdateTime > _UpdateInterval )  // update ui every second 
            {
                 _LastUpdateTime  = current;
                 List<Reading> copy = _Readings;  // Get current buffer and make it invisible to other threads by creating a new list. 
                // Since this is the only thread that does write to it this is a safe operation.


                 _Readings = new List<Reading>(); // publish a new empty list 

                 Dispatcher.Invoke(new Action(() =>
                 {
                    // This is called as part of a Window message in the main UI thread
                    // once per second now and not every 6 ms. Now we can upate the ui
                    // with a batch of 160 points at once. 
                    // A further optimization would be to disable drawing events 
                    // while we add the points to the control and enable it after
                    // the loop
                    foreach(Reading reading in copy)
                    {
                        _readings.Enqueue(reading);
                       _dataPointsCount++;
                    }

                 }),
                 System.Windows.Threading.DispatcherPriority.Normal);                    
            }
    }
}

【讨论】:

    【解决方案2】:

    您可以尝试将处理后的数据从 BackgroundWorker ProgressChanged 事件传递到 UI 线程。

    有点像....

    // Standard warnings apply: not tested, no exception handling, etc.
    
         var locker = new object();
         var que = new ConcurrentQueue<string>();
         var worker = new BackgroundWorker();
         var proc = new Process();
    
         proc.StartInfo.FileName = "consoleApp.exe";
         proc.StartInfo.UseShellExecute = false;
         proc.StartInfo.RedirectStandardOutput = true;
         proc.StartInfo.CreateNoWindow = true;
         proc.EnableRaisingEvents = true;
    
         proc.OutputDataReceived +=
            (p, a) =>
            {
               que.Enqueue(a.Data);
               Monitor.Pulse(locker);
            };
    
         worker.DoWork +=
            (s, e) =>
            {
               var watch = Stopwatch.StartNew();
               while (!e.Cancel)
               {
                  while (que.Count > 0)
                  {
                     string data;
                     if (que.TryDequeue(out data))
                     {
                        if (!String.IsNullOrEmpty(data))
                        {
                           var xGroup = Regex.Match(data, "x: ?([-0-9]*)").Groups[1];
                           int x = int.Parse(xGroup.Value);
    
                           var yGroup = Regex.Match(data, "y: ?([-0-9]*)").Groups[1];
                           int y = int.Parse(yGroup.Value);
    
                           var zGroup = Regex.Match(data, "z: ?([-0-9]*)").Groups[1];
                           int z = int.Parse(zGroup.Value);
    
                           var reading = new Reading()
                           {
                              Time = watch.Elapsed.TotalMilliseconds,
                              X = x,
                              Y = y,
                              Z = z
                           };
    
                           worker.ReportProgress(0, reading);
                        }
                     }
                     else break;
                  }
                  // wait for data or timeout and check if the worker is cancelled.
                  Monitor.Wait(locker, 50);
               }
            };
    
         worker.ProgressChanged +=
            (s, e) =>
            {
               var reading = (Reading)e.UserState;
               // We are on the UI Thread....do something with the new reading...
            };
    
         // start everybody.....
         worker.RunWorkerAsync();
         proc.Start();
         proc.BeginOutputReadLine();
    

    【讨论】:

      【解决方案3】:

      我怀疑 UI 线程上发生了线程饥饿问题,因为您的后台线程正在编组对可观察集合的调用,这可能会强制每次重新创建底层 CollectionView。这可能是一项非常昂贵的操作。

      取决于您如何配置 XAML 也是一个问题。仅测量/布局更改可能会杀死您。我可以想象,按照数据传入的速度,UI 没有机会正确评估底层数据发生的情况。

      我建议不要直接将视图绑定到队列。不要像您建议的那样使用可观察队列,而是考虑:

      1. 使用将内容限制为 50 项的常规队列。不要担心 UI 线程上发生的 NotifyCollectionChanged 事件。您也不必将每个项目编组到 UI 线程。

      2. 在您的 ViewModel 中公开一个 CollectionViewSource 对象,该对象将队列作为其集合。

      3. 在 UI 上使用计时器线程来手动强制刷新 CollectionViewSource。从每秒一次开始并减少间隔以查看您的 XAML 和机器可以处理什么。通过这种方式,您可以控制 CollectionView 的创建和销毁时间。

      【讨论】:

        【解决方案4】:

        据我所知,这是怎么回事:

        1. IU 线程启动后台工作程序以启动控制台应用程序。
        2. 它重定向控制台的输出并使用 UI 线程上的处理程序进行处理
        3. 然后,UI 线程上的处理程序每​​秒调用 Dispatcher.Invoke 160 次,以更新同一线程上的队列对象。
        4. 50 次调用后队列开始阻塞,同时 UI 将项目从队列中取出

        问题似乎是:

        让 UI 线程处理来自控制台的原始输出队列图表的更新。

        一旦 UI 超过 50 个数据项,入队和出队之间的阻塞也存在潜在问题,这可能会导致级联故障。 (我看不到足够的代码来确定这一点)

        分辨率:

        1. 启动另一个后台线程来管理控制台应用程序中的数据
        2. 新线程应该: 创建队列;处理 OutputDataReceived 事件;并启动控制台应用进程。
        3. 事件处理程序应该使用 Dispatcher.Invoke 来更新队列。应使用直接线程安全调用。
        4. 更新 UI 时,队列确实需要是非阻塞的,但我真的没有足够的信息来评论它是如何实现的。

        希望这会有所帮助 -克里斯

        【讨论】:

        • 如果我在后台线程中创建队列,UI 是否仍然可以访问它?因为队列是ObservableCollection,所以我需要图表来通知它何时更新,以便它可以自动更新自己。
        • 将参数中的主线程委托传递给后台工作人员。当队列有足够的数据时调用它。您需要确保更新是线程安全的。
        猜你喜欢
        • 1970-01-01
        • 2023-04-03
        • 2023-03-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多