C#在单独的线程中实时绘制多个图表
C# Drawing multiple charts in real time in a separate thread
我有一个采集设备,可以读取数据并将其添加到缓冲区。这是在一个单独的线程中完成的。一旦这些数据出队,我设置了一个引发事件 OnDataRead() 的委托。
在我的信号监视器中,当接收到事件时,我想在多个图表(总共 16 个)中绘制数据。因为我有 16 个图表,而不是每次收到新数据时都更新图表,我将它们添加到数据和时间戳的缓冲区中。通过读取存储在缓冲区中的数据和时间戳,每 100 毫秒在一个单独的线程中更新图表。然而,当我绘制数据时,一些图表停止添加数据并且在所有图表中,很多值都没有显示。这是一个不好的方法吗?什么是更好的方法,或者我应该改变什么才能让它发挥作用?
我有 256 个样本/秒 * 16 个通道。
这就是我得到的
这是我所期望的,但对于所有 16 个频道
public void OnDataRead(object source, DataEventArgs e)
{
if ((e.rawData.Length > 0) && (!_shouldStop))
{
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
lock (_bufferRawData)
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
lock (_bufferXValues)
_bufferXValues.Add(DateTime.Now);
}
}
private void AddDataThreadLoop()
{
while (!_shouldStop)
{
chChannels[1].Invoke(addDataDel);
Thread.Sleep(100); //sleeps for 100ms
}
}
private void AddData()
{
// Copy data stored in lists to arrays
if (_bufferRawData.Count > 0)
{
float[] rawData;
lock (_bufferRawData)
{
rawData = _bufferRawData.ToArray();
_bufferRawData.Clear();
}
DateTime[] xValues;
lock (_bufferXValues)
{
xValues = _bufferXValues.ToArray();
_bufferXValues.Clear();
}
// Add new data points for the selected channel chart
int channelIdx = 0;
for (int sampleIdx = 0; sampleIdx < rawData.Length -1; sampleIdx++)
{
// Calculate the channel where the smaple corersponds
channelIdx = sampleIdx % (_numChannels + 1);
foreach (Series ptSeries in chChannels[channelIdx].Series)
// Add new datapoint to the corresponding chart (x, y, chartIndex, seriesIndex)
AddNewPoint(xValues[sampleIdx], rawData[sampleIdx], channelIdx, ptSeries);
}
}
}
public void AddNewPoint(DateTime timeStamp, float yValue, int chartIDx, System.Windows.Forms.DataVisualization.Charting.Series ptSeries)
{
//Add datapoint
ptSeries.Points.AddXY(timeStamp.ToOADate(), yValue);
// Remove old datapoints if needed
double removeBefore = timeStamp.AddSeconds((double)(8) * (-1)).ToOADate();
while (ptSeries.Points[0].XValue < removeBefore)
{
ptSeries.Points.RemoveAt(0);
}
// Modify minimum and maximum for new samples
chChannels[chartIDx].ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue;
chChannels[chartIDx].ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddSeconds(10).ToOADate();
chChannels[chartIDx].ChartAreas[0].AxisY.Maximum = _yMax;
chChannels[chartIDx].ChartAreas[0].AxisY.Minimum = -_yMax;
chChannels[chartIDx].Invalidate();
}
private void btnPlay_Click(object sender, EventArgs e)
{
//Create thread
//define a thread to add values into chart
ThreadStart addDataThreadObj = new ThreadStart(AddDataThreadLoop);
addDataRunner = new Thread(addDataThreadObj);
addDataDel += new AddDataDelegate(AddData);
//Start thread
addDataRunner.Start();
}
编辑 1:
chChanels 是图表列表,其中每个元素对应于其中一个图表。
public 列出 chChannels;
EDIT2
为此更改锁定后,所有图表都会更新。但是每个图表的很多样本仍然没有更新。
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
很难说这些是否是您唯一的问题,但至少这对我来说很突出:
您正在为时间戳和原始数据缓冲区使用两个单独的锁。这意味着缓冲区可能不代表同一系列的数据。您可能希望使用一个锁来访问两个缓冲区:
lock (_bufferRawData) {
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
事实上,我还建议将锁定移出 for 循环。每次迭代都锁定是非常低效的:
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
- 您正在做的另一件事可以改进(但不一定解释为什么数据看起来很奇怪)是您在安排更新的线程中休眠。您可能应该将其替换为定期触发并进行更新的计时器。由于您在 UI 线程上调用更新,因此您可能会受益于使用 UI 计时器。
代码中有两个问题:
1) 以低效的方式使用锁。我把两把单锁换成一把普通锁。正如其中一个答案所指出的,我还为所有迭代锁定它,而不是为每个迭代都锁定它。应该是这样
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
2) 我生成的时间戳与收集数据时的时间戳不一致。因此,多个 Y 样本具有相同或相似的时间戳。
一种解决方案是为每个样本生成准确的时间戳。这应该在获取样本时完成。但是,由于频率为 256Hz,因此对应于每 4 毫秒 1 个样本。 DateTime.Now 的精度在 15ms 左右,这使得这种方法不适用于 256Hz 的频率。但是,对于较低的频率,这将是一个很好的方法。
正确的方法是在绘制数据时确定图形的 X 轴值。由于频率固定在256Hz,所以很容易知道Y样本的X值。
我有一个采集设备,可以读取数据并将其添加到缓冲区。这是在一个单独的线程中完成的。一旦这些数据出队,我设置了一个引发事件 OnDataRead() 的委托。
在我的信号监视器中,当接收到事件时,我想在多个图表(总共 16 个)中绘制数据。因为我有 16 个图表,而不是每次收到新数据时都更新图表,我将它们添加到数据和时间戳的缓冲区中。通过读取存储在缓冲区中的数据和时间戳,每 100 毫秒在一个单独的线程中更新图表。然而,当我绘制数据时,一些图表停止添加数据并且在所有图表中,很多值都没有显示。这是一个不好的方法吗?什么是更好的方法,或者我应该改变什么才能让它发挥作用?
我有 256 个样本/秒 * 16 个通道。
这就是我得到的
这是我所期望的,但对于所有 16 个频道
public void OnDataRead(object source, DataEventArgs e)
{
if ((e.rawData.Length > 0) && (!_shouldStop))
{
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
lock (_bufferRawData)
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
lock (_bufferXValues)
_bufferXValues.Add(DateTime.Now);
}
}
private void AddDataThreadLoop()
{
while (!_shouldStop)
{
chChannels[1].Invoke(addDataDel);
Thread.Sleep(100); //sleeps for 100ms
}
}
private void AddData()
{
// Copy data stored in lists to arrays
if (_bufferRawData.Count > 0)
{
float[] rawData;
lock (_bufferRawData)
{
rawData = _bufferRawData.ToArray();
_bufferRawData.Clear();
}
DateTime[] xValues;
lock (_bufferXValues)
{
xValues = _bufferXValues.ToArray();
_bufferXValues.Clear();
}
// Add new data points for the selected channel chart
int channelIdx = 0;
for (int sampleIdx = 0; sampleIdx < rawData.Length -1; sampleIdx++)
{
// Calculate the channel where the smaple corersponds
channelIdx = sampleIdx % (_numChannels + 1);
foreach (Series ptSeries in chChannels[channelIdx].Series)
// Add new datapoint to the corresponding chart (x, y, chartIndex, seriesIndex)
AddNewPoint(xValues[sampleIdx], rawData[sampleIdx], channelIdx, ptSeries);
}
}
}
public void AddNewPoint(DateTime timeStamp, float yValue, int chartIDx, System.Windows.Forms.DataVisualization.Charting.Series ptSeries)
{
//Add datapoint
ptSeries.Points.AddXY(timeStamp.ToOADate(), yValue);
// Remove old datapoints if needed
double removeBefore = timeStamp.AddSeconds((double)(8) * (-1)).ToOADate();
while (ptSeries.Points[0].XValue < removeBefore)
{
ptSeries.Points.RemoveAt(0);
}
// Modify minimum and maximum for new samples
chChannels[chartIDx].ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue;
chChannels[chartIDx].ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddSeconds(10).ToOADate();
chChannels[chartIDx].ChartAreas[0].AxisY.Maximum = _yMax;
chChannels[chartIDx].ChartAreas[0].AxisY.Minimum = -_yMax;
chChannels[chartIDx].Invalidate();
}
private void btnPlay_Click(object sender, EventArgs e)
{
//Create thread
//define a thread to add values into chart
ThreadStart addDataThreadObj = new ThreadStart(AddDataThreadLoop);
addDataRunner = new Thread(addDataThreadObj);
addDataDel += new AddDataDelegate(AddData);
//Start thread
addDataRunner.Start();
}
编辑 1: chChanels 是图表列表,其中每个元素对应于其中一个图表。 public 列出 chChannels;
EDIT2 为此更改锁定后,所有图表都会更新。但是每个图表的很多样本仍然没有更新。
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
很难说这些是否是您唯一的问题,但至少这对我来说很突出:
您正在为时间戳和原始数据缓冲区使用两个单独的锁。这意味着缓冲区可能不代表同一系列的数据。您可能希望使用一个锁来访问两个缓冲区:
lock (_bufferRawData) { // Append data _bufferRawData.Add(e.rawData[sampleIdx]); _bufferXValues.Add(DateTime.Now); }
事实上,我还建议将锁定移出 for 循环。每次迭代都锁定是非常低效的:
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
- 您正在做的另一件事可以改进(但不一定解释为什么数据看起来很奇怪)是您在安排更新的线程中休眠。您可能应该将其替换为定期触发并进行更新的计时器。由于您在 UI 线程上调用更新,因此您可能会受益于使用 UI 计时器。
代码中有两个问题:
1) 以低效的方式使用锁。我把两把单锁换成一把普通锁。正如其中一个答案所指出的,我还为所有迭代锁定它,而不是为每个迭代都锁定它。应该是这样
lock (_bufferRawData) {
for (int sampleIdx = 0; sampleIdx < e.rawData.Length; sampleIdx++)
{
// Append data
_bufferRawData.Add(e.rawData[sampleIdx]);
_bufferXValues.Add(DateTime.Now);
}
}
2) 我生成的时间戳与收集数据时的时间戳不一致。因此,多个 Y 样本具有相同或相似的时间戳。
一种解决方案是为每个样本生成准确的时间戳。这应该在获取样本时完成。但是,由于频率为 256Hz,因此对应于每 4 毫秒 1 个样本。 DateTime.Now 的精度在 15ms 左右,这使得这种方法不适用于 256Hz 的频率。但是,对于较低的频率,这将是一个很好的方法。
正确的方法是在绘制数据时确定图形的 X 轴值。由于频率固定在256Hz,所以很容易知道Y样本的X值。