Multithreading 读取控制台应用程序的输出和WPF绘制异步
我有一个控制台应用程序,每1秒输出大约160行信息 数据输出是可用于在图形上绘图的点 在我的WPF应用程序中,我已经成功地将其连接起来,并且正在绘制控制台应用程序的数据输出,但是,在大约500个数据点之后,我看到应用程序和UI线程锁定的速度明显减慢 我假设这是由于我使用的异步操作造成的:Multithreading 读取控制台应用程序的输出和WPF绘制异步,multithreading,wpftoolkit,Multithreading,Wpftoolkit,我有一个控制台应用程序,每1秒输出大约160行信息 数据输出是可用于在图形上绘图的点 在我的WPF应用程序中,我已经成功地将其连接起来,并且正在绘制控制台应用程序的数据输出,但是,在大约500个数据点之后,我看到应用程序和UI线程锁定的速度明显减慢 我假设这是由于我使用的异步操作造成的: BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += delegate(object s, DoWorkEventArgs ar
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);
}
}
\u读数
是中定义的自定义可观察设备
。我修改了它,一次只能有50个项目在队列中。因此,如果正在添加一个新项目且队列计数>=50,则在排队()之前调用Dequeue()
有什么方法可以提高性能,还是因为console应用程序的输出量太大而注定要失败?从这里我可以看出,这是怎么回事:
IU线程启动后台工作程序来启动控制台应用程序
它重定向控制台的输出,并使用UI线程上的处理程序对其进行处理
然后UI线程上的处理程序调用Dispatcher.Invoke每秒160次,以更新同一线程上的队列对象
50次调用后,队列开始阻塞,同时UI将项目移出队列
问题似乎是:
让UI线程处理来自控制台和队列的原始输出以及对图形的更新
此外,当用户界面后面的数据项超过50个时,排队和退队之间的阻塞可能会导致级联故障。(我看不到足够的代码来确定这一点)
决议:
启动另一个后台线程来管理控制台应用程序中的数据
新线程应该:创建队列;处理OutputDataReceived事件;并启动控制台应用程序进程
事件处理程序不应使用Dispatcher.Invoke更新队列。应使用直接线程安全调用
在更新UI时,队列确实需要是非阻塞的,但是我没有足够的信息来说明如何实现它
希望这有帮助
-克里斯从这里我可以看出,事情是这样的:
IU线程启动后台工作程序来启动控制台应用程序
它重定向控制台的输出,并使用UI线程上的处理程序对其进行处理
然后UI线程上的处理程序调用Dispatcher.Invoke每秒160次,以更新同一线程上的队列对象
50次调用后,队列开始阻塞,同时UI将项目移出队列
问题似乎是:
让UI线程处理来自控制台和队列的原始输出以及对图形的更新
此外,当用户界面后面的数据项超过50个时,排队和退队之间的阻塞可能会导致级联故障。(我看不到足够的代码来确定这一点)
决议:
启动另一个后台线程来管理控制台应用程序中的数据
新线程应该:创建队列;处理OutputDataReceived事件;并启动控制台应用程序进程
事件处理程序不应使用Dispatcher.Invoke更新队列。应使用直接线程安全调用
在更新UI时,队列确实需要是非阻塞的,但是我没有足够的信息来说明如何实现它
希望这有帮助
-Chris我怀疑UI线程上出现了线程饥饿问题,因为您的后台线程正在封送对可观察集合的调用,这可能会迫使每次都重新创建底层CollectionView。这可能是一个相当昂贵的操作
根据您如何配置XAML也是一个问题。单是测量/布局的改变就可能让你丧命。我可以想象,以数据输入的速度,UI没有机会正确地评估底层数据发生了什么
我建议不要直接将视图绑定到队列。与您建议的使用可观察队列不同,请考虑:
使用将内容限制在50项的常规队列。不要担心UI线程上发生NotifyCollectionChanged事件。您也不必将每个项目封送到UI线程
在ViewModel中公开将队列作为其集合的CollectionViewSource对象
在UI上使用计时器线程手动强制刷新CollectionViewSource。从每秒一次开始,并缩短间隔,以查看XAML和机器可以处理什么。通过这种方式,您可以控制CollectionView的创建和销毁时间
我怀疑UI线程上发生了线程饥饿问题,因为您的后台线程正在封送对可观察集合的调用,这可能会迫使每次都重新创建底层CollectionView。这可能是一个相当昂贵的操作
根据您如何配置XAML也是一个问题。单是测量/布局的改变就可能让你丧命。我可以想象,以数据输入的速度,UI没有机会正确地评估底层数据发生了什么
我建议不要直接将视图绑定到队列。与您建议的使用可观察队列不同,请考虑:
使用将内容限制在50项的常规队列。不要担心UI线程上发生NotifyCollectionChanged事件。您也不必将每个项目封送到UI线程
在视图中公开CollectionViewSource对象
// 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();
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);
}
}
}