长时间运行的任务阻塞了 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 并实例化 PerformanceCountertimer.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) =&gt; UpdateProgressBar(valuePBar)); 然后private void UpdateProgressBar(Int32 valuePBar) Task.Factory.StartNew(() =&gt; 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&lt;T&gt; 获取进度更新。 仅在绝对必要的情况下使用 TaskSchedulers 或 SynchronizationContexts(这里没有必要)。 切勿使用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&lt;int&gt;

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 asyncIProgress&lt;T&gt; 是一种报告进度更新的方式; MyMethod 仍然像以前一样计算进度更新值。 知道了。我制作了OnTimerTickElapsed async,但没有任何改变。再次,问题没有得到解决。让我们尝试忽略进度条(顺便说一句,感谢您的建议)。您是否成功地通过PerformanceCounter 获得了完全响应的用户界面? @Valentina: NextValue 不可能使用此代码阻止 UI。如果您的 UI 被阻塞,则可能是其他一些代码在执行此操作。请发布一个最小的、可重现的示例。【参考方案2】:

三件事……

    简化进度条的线程同步上下文。只需使用Task.Run(() =&gt; // 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(Action CallBack) Task.Factory.StartNew( () => Thread.Sleep(1000); CallBack(20); Thread.睡眠(1000);回调(40);线程睡眠(1000);更新UICallBack(60);线程睡眠(1000);回调(80);线程睡眠(1000);回调(100);); ` 请尝试将 myMethod 添加到您的代码中。我仍然在经历寒冷。我很确定是仪表引起了问题。如果我不启动计时器,一切正常【参考方案4】:

Somebody has already investigated this problem

问题在于 PerformanceCounter 初始化很复杂并且需要很多时间。但尚不清楚的是,此初始化是阻塞所有线程还是仅阻塞拥有线程(创建 PerformanceCounter 的线程)。

但我认为您的代码已经提供了答案。由于您已经在线程池线程中创建了 PerformanceCounter,它仍然会阻塞 UI。那么假设初始化阻塞所有线程可能是正确的。

如果初始化阻塞了所有线程,那么解决方案是在应用程序启动时初始化 PerformanceCounter。要么不使用默认构造函数(选择一个不仅构造实例而且初始化它的构造函数)或在启动期间调用 NextValue。

【讨论】:

我今天遇到了同样的问题。似乎只有当前线程被阻塞:将性能计数器的初始化包装在 Task.Run() 中可以解决问题。

以上是关于长时间运行的任务阻塞了 UI的主要内容,如果未能解决你的问题,请参考以下文章

Javascript Promises库在浏览器中制作“长时间运行代码 - 非阻塞UI”?

python的sched模块可​​以在任务执行期间不阻塞地运行异步任务吗?

异步任务阻塞 UI 线程

如何追踪阻塞主(UI)线程的调用?

执行ALTER TABLE语句时如何避免长时间阻塞并发查询

delphi query阻塞执行 长时间执行sql的解决办法