长时间运行的任务正在阻止 UI
本文关键字:UI 运行 任务 长时间 | 更新日期: 2023-09-27 18:35:13
我是TPL的新手,我正在尝试使用并行性测试一个非常简单的应用程序。
我正在使用WinForms,C#,VS2015
在我的表单中,我有一个进度条,一个计时器和一个仪表。我正在使用Infragistics 15.2控件。
- 该按钮将启动一个执行一些工作的功能
- 表单加载将启动 Windows.Form.Timer 并实例化
PerformanceCounter
- 在
timer.Tick
(我设置了 500 毫秒的间隔)上,我从性能计数器读取 CPU 使用率并更新仪表控件的值。
我的问题是第一次访问 CPU 计数器的 NextValue() 确实非常耗时并冻结 UI。我期待有一个完整的响应式 UI,但它仍然冻结。我肯定错过了一些东西,但我找不到什么。我很确定阻止操作是NextValue()
:如果我用随机数生成器替换它,我的 UI 是完全响应的。
你能帮帮我吗?
public partial class Form1 : Form
{
PerformanceCounter _CPUCounter;
public Form1()
{
InitializeComponent();
}
private async void ultraButton1_Click(object sender, EventArgs e)
{
var progress = new Progress<int>(valuePBar => {ultraProgressBar1.Value = valuePBar; });
await Task.Run(() => UpdatePBar(progress));
}
private void Form1_Load(object sender, EventArgs e)
{
Task.Run(() =>
{
_CPUCounter = new PerformanceCounter();
_CPUCounter.CategoryName = "Processor";
_CPUCounter.CounterName = "% Processor Time";
_CPUCounter.InstanceName = "_Total";
BeginInvoke(new Action(() =>
{
timer1.Interval = 500;
timer1.Start();
}));
});
}
private async void timer1_Tick(object sender, EventArgs e)
{
float usage = await Task.Run(() => _CPUCounter.NextValue());
RadialGauge r_gauge = (RadialGauge)ultraGauge1.Gauges[0];
r_gauge.Scales[0].Markers[0].Value = usage;
}
public void UpdatePBar(IProgress<int> progress)
{
for (Int32 seconds = 1; seconds <= 10; seconds++)
{
Thread.Sleep(1000); //simulate Work, do something with the data received
if (seconds != 10 && progress != null)
{
progress.Report(seconds * 10);
}
}
}
}
你应该:
- 从不从后台线程更新 UI 控件。
- 更喜欢
Task.Run
而不是Task.Factory.StartNew
. - 如果您想"返回 UI 线程",请首选
async
/await
。 - 使用
IProgress<T>
进行进度更新。 - 仅在绝对必要时使用
TaskScheduler
s 或SynchronizationContext
s(此处不需要)。 - 切勿使用
Control.BeginInvoke
或Control.Invoke
。
在这种情况下,计时器可以使用 Task.Run
在线程池线程上运行NextValue
,然后使用结果值更新 UI:
private async void OnTimerTickElapsed(Object sender, EventArgs e)
{
float usage = await Task.Run(() => _CPUCounter.NextValue());
RadialGauge r_gauge = (RadialGauge)this.ultraGauge.Gauges[0];
r_gauge.Scales[0].Markers[0].Value = usage;
}
对于您的进度条,请让您的MyMethod
IProgress<int>
:
private void ButtonClick(Object sender, EventArgs e)
{
var progress = new Progress<int>(valuePBar => { this.progressBar.Value = valuePBar; });
MyMethod(progress);
}
MyMethod(IProgress<int> progress)
{
...
progress.Report(50);
...
}
三件事...
- 简化进度栏的线程同步上下文。 只需使用
Task.Run(() => { // progress bar code }
- 检查
CPUCounter
是否未static
- 确保您的
_CPUCounter.NextValue()
方法支持异步并处于等待状态,否则它将阻塞。
我也更喜欢Task.Run
而不是Task.Factory.StartNew
因为它有更好的默认值(它在内部调用Task.Factory.StartNew
)
private void OnTimerTickElapsed(Object sender, EventArgs e)
{
await Task.Run(async () =>
{
float usage = await _CPUCounter.NextValue();
RadialGauge r_gauge = (RadialGauge)this.ultraGauge.Gauges[0];
r_gauge.Scales[0].Markers[0].Value = usage;
}, TaskCreationOptions.LongRunning);
}
看看你使用的间隔。 50ms 对于 TPL 的开销来说有点快。
尝试了您的代码(只是将仪表替换为显示usage
变量的简单标签,当然没有单击该按钮,因为您没有提供MyMethod
),并且没有遇到任何 UI 阻止。
发布用于更新 UI 的正确代码(即使使用 UI 调度程序 IMO 也为此目的使用 Task
是一种矫枉过正):
private void UpdateProgressBar(Int32 valuePBar)
{
BeginInvoke(new Action(() =>
{
this.progressBar.Value = valuePBar;
}));
}
private void OnTimerTickElapsed(Object sender, EventArgs e)
{
Task.Run(() =>
{
float usage = _CPUCounter.NextValue();
BeginInvoke(new Action(() =>
{
RadialGauge r_gauge = (RadialGauge)this.ultraGauge.Gauges[0];
r_gauge.Scales[0].Markers[0].Value = usage;
}));
});
}
不过,代码中没有可能导致 UI 阻塞的明显原因。除了最终这部分
_CPUCounter = new PerformanceCounter();
_CPUCounter.CategoryName = "Processor";
_CPUCounter.CounterName = "% Processor Time";
_CPUCounter.InstanceName = "_Total";
它运行在OnFormLoad
的 UI 线程上。您可以尝试在单独的任务上移动该代码,如下所示
private void OnFormLoad(Object sender, EventArgs e)
{
Task.Run(() =>
{
_CPUCounter = new PerformanceCounter();
_CPUCounter.CategoryName = "Processor";
_CPUCounter.CounterName = "% Processor Time";
_CPUCounter.InstanceName = "_Total";
BeginInvoke(new Action(() =>
{
this.timer.Interval = 500;
this.timer.Start();
}));
});
}
有人已经调查过这个问题
问题是性能计数器初始化很复杂,需要花费大量时间。但不清楚的是,此初始化是阻止所有线程还是仅阻止拥有线程(创建性能计数器的线程)。
但我认为您的代码已经提供了答案。由于您已经在线程池线程中创建性能计数器,并且它仍然阻止 UI。那么假设初始化阻止所有线程可能是正确的。
如果初始化阻止所有线程,则解决方案是在应用程序启动时初始化性能计数器。不使用默认构造函数(选择一个不仅构造实例而且初始化实例的构造函数),或者在启动期间调用一次 NextValue。