如何防止 backgroundWorker 导致 UI 变得缓慢?
How can I prevent the backgroundWorker causing the UI to become sluggish?
我制作了一个 C# WinForms 应用程序,我在其中使用图表绘制了数千个实时数据点。我注意到在我的申请期间 运行 当我打开网络浏览器时,情节冻结了。我试图绘制更少的点,但似乎永远不知道将执行哪个并行程序,所以我担心 CPU 根据 PC 使用其他程序会影响性能。
编辑:
private void button1_Click(object sender, EventArgs e)
{
///
_cts = new CancellationTokenSource();
_infiniteLoop = InfiniteLoop(_cts.Token);
}
private async Task InfiniteLoop(CancellationToken cancellationToken = default)
{
ushort[] ushortArray = null;
while (true)
{
Task loopMinimumDurationTask = Task.Delay(100, cancellationToken);
Task<ushort []> calculationTask = Task.Run(() => Calculate());
if (ushortArray != null) PlotData(ushortArray);
ushortArray = await calculationTask;
await loopMinimumDurationTask;
}
}
public ushort [] Calculate()
{
init();
daq.ALoadQueue(chArray, chRange, CHANCOUNT);
ScanOptions options = ScanOptions.Background | ScanOptions.Continuous | ScanOptions.ConvertData;
//setup the acquisiton
UL = daq.AInScan(FIRSTCHANNEL, SINGLE_KANAL_NUM, BUFFERSIZE, ref Rate, Range.Bip10Volts, buffer, options);
UL = daq.GetStatus(out daqStatus, out Count, out Index, FunctionType.AiFunction);
if ((Index >= HALFBUFFSIZE) & ReadLower) //check for 50% more data
{
//get lower half of buffer
UL = MccService.WinBufToArray(buffer, ushortArray, 0, HALFBUFFSIZE);
ReadLower = false; //flag that controls the next read
return ushortArray;
}
else if ((Index < HALFBUFFSIZE) & !ReadLower)
{
//get the upper half
UL = MccService.WinBufToArray(buffer, ushortArray, HALFBUFFSIZE, HALFBUFFSIZE);
ReadLower = true;//flag that controls the next read
return ushortArray;
}
return null;
}
public void PlotData(ushort[] datArray_Plot)
{
////////Thread.Sleep(10);
SerialList1.Clear();
for (int b = 0; b < HALFBUFFSIZE; b++)
{
UL = (daq.ToEngUnits(Range.Bip10Volts, datArray_Plot[b], out temp2));
SerialList1.Add(temp2);
SerialList2.Add(temp2);
ikb_p = ikb_p + 1;
}
int out_size = SerialList1.Count / h; //size of downsampled array
if (out_size <= 2)
out_size = 2;
array = SerialList1.ToArray(); //original array
if (h != 1)
array = Downsample(array, out_size); //downsampled array
if (ikb_p > BUFFERSIZE)
{
chart1.Series["Ch0"].Points.SuspendUpdates();
for (int b = 0; b < out_size; b++)
{
chart1.Series["Ch0"].Points.AddY(array[b]); //Plots each sample or use chart1.Series["Ch0"].Points.DataBindY(array);
if (chart1.Series["Ch0"].Points.Count > display_seconds * FREQ / h)
{
chart1.Series["Ch0"].Points.RemoveAt(0);
}
}
//chart1.Series["Ch0"].Points.ResumeUpdates();
chart1.Invalidate();
}
//FFT
if (SerialList2.Count > 4 * HALFBUFFSIZE / CHANCOUNT)
{
chart2.Series["Freq"].Points.Clear();
float sampling_freq = (float)FREQ;
float[] data = SerialList2.ToArray();
double[] dftIn = new double[data.Length];
double[] dftInIm = new double[data.Length];
double[] DftIn = new double[data.Length];
double[] FFTResult = new double[data.Length];
double[] f = new double[data.Length];
double[] power = new double[data.Length];
double[] window = MathNet.Numerics.Window.Hamming(data.Length);
for (int i = 0; i < data.Length; i++)
{
dftIn[i] = window[i] * (double)data[i];
}
for (int i = 0; i < data.Length; i++)
{
dftInIm[i] = 0.0;
}
FFT(dftIn, dftInIm, out reFFT, out imFFT, (int)Math.Log(data.Length, 2));
for (int i = 0; i < data.Length / 2; i++)
{
if (i > 0)
{
float a = sampling_freq / (float)data.Length;
float x = (float)i * a;
double y = Math.Sqrt(reFFT[i] * reFFT[i] + imFFT[i] * imFFT[i]);
f[i] = x;
FFTResult[i] = 2 * y / (data.Length / 2);
power[i] = 0.5 * FFTResult[i] * FFTResult[i];
}
}
double scale = data.Length / sampling_freq;
chart2.Series["Freq"].Points.DataBindXY(f, power);
float stdCh0 = 0;
float avg1 = SerialList2.Average();
float max1 = SerialList2.Max();
float min1 = SerialList2.Min();
float sum1 = (float)SerialList2.Sum(d => Math.Pow(d - avg1, 2));
stdCh0 = (float)Math.Sqrt((sum1) / (SerialList2.Count() - 1));
label5.Text = avg1.ToString("0.000000");
label22.Text = stdCh0.ToString("0.000000");
label70.Text = max1.ToString("0.000000");
label61.Text = min1.ToString("0.000000");
SerialList2.Clear();
label1.Text = count_sample.ToString();
}
///progressBar1
double ratio = (double)count_sample / (seconds * FREQ);
if (ratio > 1.000)
ratio = 1;
progressBar1.Value = (Convert.ToInt32(1000 * ratio));
progressBar1.Invalidate();
progressBar1.Update();
//Display event handlers
if (comboBox2_changed == true)
{
if (comboBox2.SelectedIndex == 0)
{
//chart1.ChartAreas[0].RecalculateAxesScale();
chart1.ChartAreas[0].AxisY.IsStartedFromZero = false;
}
if (comboBox2.SelectedIndex == 1)
{
//chart1.ChartAreas[0].RecalculateAxesScale();
chart1.ChartAreas[0].AxisY.IsStartedFromZero = true;
}
comboBox2_changed = false;
}
if (comboBox1_changed == true)
{
if (comboBox1.SelectedIndex == 0)
{
chart1.Series["Ch0"].ChartType = SeriesChartType.FastLine;
}
else
chart1.Series["Ch0"].ChartType = SeriesChartType.FastPoint;
}
if (num_updown1_changed)
{
display_seconds = (float)numericUpDown1.Value * 0.001f;
h = (int)numericUpDown2.Value;
chart1.Series["Ch0"].Points.Clear();
//chart1.ChartAreas[0].AxisX.Maximum = display_seconds * FREQ / h;
num_updown1_changed = false;
int avg = (int)((double)FREQ * (Decimal.ToDouble(numericUpDown1.Value) / 1000.0) / max_chart_points);
if (avg != 0)
numericUpDown2.Value = avg;
}
if (num_updown2_changed)
{
display_seconds = (float)numericUpDown1.Value * 0.001f;
h = (int)numericUpDown2.Value;
chart1.Series["Ch0"].Points.Clear();
//chart1.ChartAreas[0].AxisX.Maximum = display_seconds * FREQ / h;
num_updown2_changed = false;
}
}
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
_cts.Cancel();
// Wait the completion of the loop before closing the form
try { _infiniteLoop.GetAwaiter().GetResult(); }
catch (OperationCanceledException) { } // Ignore this error
}
您可以使用 threadpriority:
Thread.CurrentThread.Priority = ThreadPriority.Highest;
然而,在大多数情况下,这被认为是一种糟糕的形式,因为操作系统可以更好地决定哪个程序值得 CPU 时间。即使您明确要求,它也不需要更多时间来满足您的要求。
如果绘图需要花费大量时间,您可以考虑:
- 你能以某种方式优化绘图吗?
- 你能减少点数吗?
- 您或许可以绘制数据集的一小部分?
- 您可以预处理绘图以减少点密度。屏幕通常有 2k-4k 的分辨率,所以如果你有一个包含更多点的折线图,用户无论如何都看不到它。
我的建议是废弃 obsolete BackgroundWorker
,支持无限异步循环。下面的示例假设存在一个 Calculate
方法应该 运行 在后台线程上并且应该 return 一个计算的结果,以及一个 UpdateUI
方法应该 运行 在 UI 线程上并且应该使用这个结果。
private async Task InfiniteLoop(CancellationToken cancellationToken = default)
{
object calculationResult = null;
while (true)
{
Task loopMinimumDurationTask = Task.Delay(100, cancellationToken);
Task<object> calculationTask = Task.Run(() => Calculate());
if (calculationResult != null) UpdateUI(calculationResult);
calculationResult = await calculationTask;
await loopMinimumDurationTask;
}
}
本设计具有以下特点:
Calculate
和 UpdateUI
方法并行工作。
- 如果
Calculate
先完成,则等待 UpdateUI
完成后再开始下一次计算。
- 如果
UpdateUI
先完成,它会等待 Calculate
完成,然后再开始 UI. 的下一次更新
- 如果
Calculate
和 UpdateUI
都在 100 毫秒内完成,则会施加额外的异步延迟,因此每秒不会发生超过 10 个循环。
- 无限循环可以通过取消可选的
CancellationToken
来终止。
上例中calculationResult
变量的object
类型只是为了演示。除非计算结果微不足道,否则您应该创建一个 class 或结构来存储在每个循环中更新 UI 所需的所有数据。通过消除所有全局状态,您可以最大限度地减少可能出错的次数。
用法示例:
private CancellationTokenSource _cts;
private Task _infiniteLoop;
private void Form_Load(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
_infiniteLoop = InfiniteLoop(_cts.Token);
}
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
_cts.Cancel();
// Wait the completion of the loop before closing the form
try { _infiniteLoop.GetAwaiter().GetResult(); }
catch (OperationCanceledException) { } // Ignore this error
}
我制作了一个 C# WinForms 应用程序,我在其中使用图表绘制了数千个实时数据点。我注意到在我的申请期间 运行 当我打开网络浏览器时,情节冻结了。我试图绘制更少的点,但似乎永远不知道将执行哪个并行程序,所以我担心 CPU 根据 PC 使用其他程序会影响性能。
编辑:
private void button1_Click(object sender, EventArgs e)
{
///
_cts = new CancellationTokenSource();
_infiniteLoop = InfiniteLoop(_cts.Token);
}
private async Task InfiniteLoop(CancellationToken cancellationToken = default)
{
ushort[] ushortArray = null;
while (true)
{
Task loopMinimumDurationTask = Task.Delay(100, cancellationToken);
Task<ushort []> calculationTask = Task.Run(() => Calculate());
if (ushortArray != null) PlotData(ushortArray);
ushortArray = await calculationTask;
await loopMinimumDurationTask;
}
}
public ushort [] Calculate()
{
init();
daq.ALoadQueue(chArray, chRange, CHANCOUNT);
ScanOptions options = ScanOptions.Background | ScanOptions.Continuous | ScanOptions.ConvertData;
//setup the acquisiton
UL = daq.AInScan(FIRSTCHANNEL, SINGLE_KANAL_NUM, BUFFERSIZE, ref Rate, Range.Bip10Volts, buffer, options);
UL = daq.GetStatus(out daqStatus, out Count, out Index, FunctionType.AiFunction);
if ((Index >= HALFBUFFSIZE) & ReadLower) //check for 50% more data
{
//get lower half of buffer
UL = MccService.WinBufToArray(buffer, ushortArray, 0, HALFBUFFSIZE);
ReadLower = false; //flag that controls the next read
return ushortArray;
}
else if ((Index < HALFBUFFSIZE) & !ReadLower)
{
//get the upper half
UL = MccService.WinBufToArray(buffer, ushortArray, HALFBUFFSIZE, HALFBUFFSIZE);
ReadLower = true;//flag that controls the next read
return ushortArray;
}
return null;
}
public void PlotData(ushort[] datArray_Plot)
{
////////Thread.Sleep(10);
SerialList1.Clear();
for (int b = 0; b < HALFBUFFSIZE; b++)
{
UL = (daq.ToEngUnits(Range.Bip10Volts, datArray_Plot[b], out temp2));
SerialList1.Add(temp2);
SerialList2.Add(temp2);
ikb_p = ikb_p + 1;
}
int out_size = SerialList1.Count / h; //size of downsampled array
if (out_size <= 2)
out_size = 2;
array = SerialList1.ToArray(); //original array
if (h != 1)
array = Downsample(array, out_size); //downsampled array
if (ikb_p > BUFFERSIZE)
{
chart1.Series["Ch0"].Points.SuspendUpdates();
for (int b = 0; b < out_size; b++)
{
chart1.Series["Ch0"].Points.AddY(array[b]); //Plots each sample or use chart1.Series["Ch0"].Points.DataBindY(array);
if (chart1.Series["Ch0"].Points.Count > display_seconds * FREQ / h)
{
chart1.Series["Ch0"].Points.RemoveAt(0);
}
}
//chart1.Series["Ch0"].Points.ResumeUpdates();
chart1.Invalidate();
}
//FFT
if (SerialList2.Count > 4 * HALFBUFFSIZE / CHANCOUNT)
{
chart2.Series["Freq"].Points.Clear();
float sampling_freq = (float)FREQ;
float[] data = SerialList2.ToArray();
double[] dftIn = new double[data.Length];
double[] dftInIm = new double[data.Length];
double[] DftIn = new double[data.Length];
double[] FFTResult = new double[data.Length];
double[] f = new double[data.Length];
double[] power = new double[data.Length];
double[] window = MathNet.Numerics.Window.Hamming(data.Length);
for (int i = 0; i < data.Length; i++)
{
dftIn[i] = window[i] * (double)data[i];
}
for (int i = 0; i < data.Length; i++)
{
dftInIm[i] = 0.0;
}
FFT(dftIn, dftInIm, out reFFT, out imFFT, (int)Math.Log(data.Length, 2));
for (int i = 0; i < data.Length / 2; i++)
{
if (i > 0)
{
float a = sampling_freq / (float)data.Length;
float x = (float)i * a;
double y = Math.Sqrt(reFFT[i] * reFFT[i] + imFFT[i] * imFFT[i]);
f[i] = x;
FFTResult[i] = 2 * y / (data.Length / 2);
power[i] = 0.5 * FFTResult[i] * FFTResult[i];
}
}
double scale = data.Length / sampling_freq;
chart2.Series["Freq"].Points.DataBindXY(f, power);
float stdCh0 = 0;
float avg1 = SerialList2.Average();
float max1 = SerialList2.Max();
float min1 = SerialList2.Min();
float sum1 = (float)SerialList2.Sum(d => Math.Pow(d - avg1, 2));
stdCh0 = (float)Math.Sqrt((sum1) / (SerialList2.Count() - 1));
label5.Text = avg1.ToString("0.000000");
label22.Text = stdCh0.ToString("0.000000");
label70.Text = max1.ToString("0.000000");
label61.Text = min1.ToString("0.000000");
SerialList2.Clear();
label1.Text = count_sample.ToString();
}
///progressBar1
double ratio = (double)count_sample / (seconds * FREQ);
if (ratio > 1.000)
ratio = 1;
progressBar1.Value = (Convert.ToInt32(1000 * ratio));
progressBar1.Invalidate();
progressBar1.Update();
//Display event handlers
if (comboBox2_changed == true)
{
if (comboBox2.SelectedIndex == 0)
{
//chart1.ChartAreas[0].RecalculateAxesScale();
chart1.ChartAreas[0].AxisY.IsStartedFromZero = false;
}
if (comboBox2.SelectedIndex == 1)
{
//chart1.ChartAreas[0].RecalculateAxesScale();
chart1.ChartAreas[0].AxisY.IsStartedFromZero = true;
}
comboBox2_changed = false;
}
if (comboBox1_changed == true)
{
if (comboBox1.SelectedIndex == 0)
{
chart1.Series["Ch0"].ChartType = SeriesChartType.FastLine;
}
else
chart1.Series["Ch0"].ChartType = SeriesChartType.FastPoint;
}
if (num_updown1_changed)
{
display_seconds = (float)numericUpDown1.Value * 0.001f;
h = (int)numericUpDown2.Value;
chart1.Series["Ch0"].Points.Clear();
//chart1.ChartAreas[0].AxisX.Maximum = display_seconds * FREQ / h;
num_updown1_changed = false;
int avg = (int)((double)FREQ * (Decimal.ToDouble(numericUpDown1.Value) / 1000.0) / max_chart_points);
if (avg != 0)
numericUpDown2.Value = avg;
}
if (num_updown2_changed)
{
display_seconds = (float)numericUpDown1.Value * 0.001f;
h = (int)numericUpDown2.Value;
chart1.Series["Ch0"].Points.Clear();
//chart1.ChartAreas[0].AxisX.Maximum = display_seconds * FREQ / h;
num_updown2_changed = false;
}
}
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
_cts.Cancel();
// Wait the completion of the loop before closing the form
try { _infiniteLoop.GetAwaiter().GetResult(); }
catch (OperationCanceledException) { } // Ignore this error
}
您可以使用 threadpriority:
Thread.CurrentThread.Priority = ThreadPriority.Highest;
然而,在大多数情况下,这被认为是一种糟糕的形式,因为操作系统可以更好地决定哪个程序值得 CPU 时间。即使您明确要求,它也不需要更多时间来满足您的要求。
如果绘图需要花费大量时间,您可以考虑:
- 你能以某种方式优化绘图吗?
- 你能减少点数吗?
- 您或许可以绘制数据集的一小部分?
- 您可以预处理绘图以减少点密度。屏幕通常有 2k-4k 的分辨率,所以如果你有一个包含更多点的折线图,用户无论如何都看不到它。
我的建议是废弃 obsolete BackgroundWorker
,支持无限异步循环。下面的示例假设存在一个 Calculate
方法应该 运行 在后台线程上并且应该 return 一个计算的结果,以及一个 UpdateUI
方法应该 运行 在 UI 线程上并且应该使用这个结果。
private async Task InfiniteLoop(CancellationToken cancellationToken = default)
{
object calculationResult = null;
while (true)
{
Task loopMinimumDurationTask = Task.Delay(100, cancellationToken);
Task<object> calculationTask = Task.Run(() => Calculate());
if (calculationResult != null) UpdateUI(calculationResult);
calculationResult = await calculationTask;
await loopMinimumDurationTask;
}
}
本设计具有以下特点:
Calculate
和UpdateUI
方法并行工作。- 如果
Calculate
先完成,则等待UpdateUI
完成后再开始下一次计算。 - 如果
UpdateUI
先完成,它会等待Calculate
完成,然后再开始 UI. 的下一次更新
- 如果
Calculate
和UpdateUI
都在 100 毫秒内完成,则会施加额外的异步延迟,因此每秒不会发生超过 10 个循环。 - 无限循环可以通过取消可选的
CancellationToken
来终止。
上例中calculationResult
变量的object
类型只是为了演示。除非计算结果微不足道,否则您应该创建一个 class 或结构来存储在每个循环中更新 UI 所需的所有数据。通过消除所有全局状态,您可以最大限度地减少可能出错的次数。
用法示例:
private CancellationTokenSource _cts;
private Task _infiniteLoop;
private void Form_Load(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
_infiniteLoop = InfiniteLoop(_cts.Token);
}
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
_cts.Cancel();
// Wait the completion of the loop before closing the form
try { _infiniteLoop.GetAwaiter().GetResult(); }
catch (OperationCanceledException) { } // Ignore this error
}