长时间运行的任务阻塞了 UI
Posted
技术标签:
【中文标题】长时间运行的任务阻塞了 UI【英文标题】:Long running task is blocking the UI 【发布时间】:2016-02-18 20:37:33 【问题描述】:我是 TPL 的新手,我正在尝试使用并行性测试一个非常简单的应用程序。
我使用的是 WinForms、C#、VS2015
在我的表单中,我有一个进度条、一个计时器和一个仪表。 我正在使用 Infragistics 15.2 控件。
该按钮将启动一个功能,它会做一些工作 Form Load 将启动 Windows.Form.Timer 并实例化PerformanceCounter
在 timer.Tick
(我设置了 500 毫秒的间隔)上,我从 PerformanceCounter 读取 CPU 使用率并更新 Gauge 控件的值。
我的问题是第一次访问 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 线程更新 UI 元素(在您的情况下为仪表)。为了测试,你注释掉仪表代码会发生什么,即只留下float usage = _CPUCounter.NextValue();
行里面?
_CPUCounter的作用域是什么?是静态的吗?
@IvanStoev UI 无论如何都冻结了。我只是留下了那行代码。顺便说一句,你建议我更新 UI 什么?对于进度条对于按钮,在Click
事件上,我调用一个启动任务的方法:MyMethod((valuePBar) => UpdateProgressBar(valuePBar));
然后private void UpdateProgressBar(Int32 valuePBar) Task.Factory.StartNew(() => this.progressBar.Value = valuePBar; , CancellationToken.None, TaskCreationOptions.None, this.ui_TaskScheduler);
@ChrisBallance 这是一个公共变量。
@Valentina 我的意思是您原始帖子 (OnTimerTickElapsed
) 中的代码无法冻结 UI。我们需要Minimal, Complete, and Verifiable example
【参考方案1】:
你应该:
永远不要从后台线程更新 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);
...
【讨论】:
感谢您的建议。不幸的是,使用await
关键字会导致编译错误,可能是由于NextValue 函数没有async
修饰符。
我不明白 IProgress 部分。 MyMethod 是计算值以更新 progressBar 的方法。
@Valentina:NextValue
不一定是async
。您看到的编译器错误可能是因为您忘记了 OnTimerTickElapsed
async
。 IProgress<T>
是一种报告进度更新的方式; MyMethod
仍然像以前一样计算进度更新值。
知道了。我制作了OnTimerTickElapsed async
,但没有任何改变。再次,问题没有得到解决。让我们尝试忽略进度条(顺便说一句,感谢您的建议)。您是否成功地通过PerformanceCounter
获得了完全响应的用户界面?
@Valentina: NextValue
不可能使用此代码阻止 UI。如果您的 UI 被阻塞,则可能是其他一些代码在执行此操作。请发布一个最小的、可重现的示例。【参考方案2】:
三件事……
-
简化进度条的线程同步上下文。只需使用
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 的开销来说有点快。
【讨论】:
好点。如果是静态的,看起来对 _CPUCounter 的访问可能会被阻塞 1) 不是静态的 2) 如何验证System.Diagnostics.PerformanceCounter
是否支持异步? 3) 500ms 的结果相同
为进度条添加代码后,这是您的问题,而不是性能计数器。请参阅我对您的代码的评论。【参考方案3】:
尝试了您的代码(只是将仪表替换为显示usage
变量的简单标签,当然没有单击按钮,因为您没有提供MyMethod
)并且没有遇到任何UI 阻塞。
发布用于更新 UI 的正确代码(为此目的使用 Task
,即使使用 UI 调度程序 IMO 也是一种矫枉过正):
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();
));
);
【讨论】:
对不起,我忘记了 MyMethod ` public static void MyMethod(ActionSomebody has already investigated this problem
问题在于 PerformanceCounter 初始化很复杂并且需要很多时间。但尚不清楚的是,此初始化是阻塞所有线程还是仅阻塞拥有线程(创建 PerformanceCounter 的线程)。
但我认为您的代码已经提供了答案。由于您已经在线程池线程中创建了 PerformanceCounter,它仍然会阻塞 UI。那么假设初始化阻塞所有线程可能是正确的。
如果初始化阻塞了所有线程,那么解决方案是在应用程序启动时初始化 PerformanceCounter。要么不使用默认构造函数(选择一个不仅构造实例而且初始化它的构造函数)或在启动期间调用 NextValue。
【讨论】:
我今天遇到了同样的问题。似乎只有当前线程被阻塞:将性能计数器的初始化包装在 Task.Run() 中可以解决问题。以上是关于长时间运行的任务阻塞了 UI的主要内容,如果未能解决你的问题,请参考以下文章
Javascript Promises库在浏览器中制作“长时间运行代码 - 非阻塞UI”?