长时间运行的任务正在阻止 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

你应该:

  • 从不从后台线程更新 UI 控件。
  • 更喜欢Task.Run而不是Task.Factory.StartNew.
  • 如果您想"返回 UI 线程",请首选 async/await
  • 使用IProgress<T>进行进度更新。
  • 仅在绝对必要时使用 TaskScheduler s 或 SynchronizationContext s(此处不需要)。
  • 切勿使用Control.BeginInvokeControl.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);
  ...
}

三件事...

  1. 简化进度栏的线程同步上下文。 只需使用Task.Run(() => { // progress bar code }
  2. 检查CPUCounter是否未static
  3. 确保您的_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。