从控制台应用程序读取输出和WPF绘制异步

时间:2011-02-19 22:46:15

标签: multithreading wpftoolkit

我有一个控制台应用程序,每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,则会在Dequeue()之前调用Enqueue()

我有什么方法可以提高性能,或者因为控制台应用程序输出多少而注定失败?

4 个答案:

答案 0 :(得分:0)

从我可以看出的是它看起来是怎么回事:

  1. IU线程旋转后台工作程序以启动控制台App。
  2. 它重定向控制台的输出并使用UI线程上的处理程序处理它
  3. UI线程上的处理程序然后每秒调用Dispatcher.Invoke 160次以更新同一线程上的队列对象。
  4. 50次调用后,队列开始阻塞,而项目由UI出列
  5. 麻烦似乎是:

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

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

    分辨率:

    1. 启动另一个后台线程来管理控制台应用程序中的数据
    2. 新线程应该:创建队列;处理OutputDataReceived事件;并启动控制台应用程序进程。
    3. 事件处理程序应该使用Dispatcher.Invoke来更新队列。应该使用直接线程安全调用。
    4. 更新用户界面时,队列确实需要非阻塞,但我真的没有足够的信息来说明如何实施评论。
    5. 希望这会有所帮助  -Chris

答案 1 :(得分:0)

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

根据您配置XAML的方式也是一个问题。单独测量/布局变化可能会让你失望。我想,按照数据传入的速度,用户界面没有机会正确评估基础数据的发生情况。

我建议不要直接将View绑定到Queue。不要像你建议的那样使用Observable Queue,而应考虑:

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

  2. 在ViewModel中展示CollectionViewSource对象,该对象将Queue作为其集合。

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

答案 2 :(得分:0)

您可以尝试将处理后的数据从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 :(得分:0)

您只需将这些积分存储在一个列表中,并且只有当您拥有以下内容时,才能调用调度程序。达到160分,因此您不会创建许多更新消息。目前你每6ms造成一次窗口消息太多了。更新用户界面时,例如每秒或每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);                    
            }
    }
}