简体   繁体   中英

Long running task is blocking the UI

I am new to TPL and I am trying to test a very simple application using parallelism.

I am using a WinForms, C#, VS2015

In my form, I have one progress bar, a timer and a gauge. I am using Infragistics 15.2 controls.

  • the button will launch a function which does some work
  • Form Load will start a Windows.Form.Timer and instantiate the PerformanceCounter
  • On timer.Tick (I set a 500ms interval), I read the CPU usage from the PerformanceCounter and update the value of the Gauge control.

My problem is that the first access to NextValue() of the CPU counter is really time expensive and freezes the UI. I was expecting to have a full responsive UI but still it freezes. I am missing something for sure, but I cannot find out what. I am pretty sure that The blocking action is the NextValue() : if I replace it with a random number generator, my UI is fully responsive.

Can you please help me?

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);
            }
        }
    }
}

You should:

  • Never update UI controls from background threads.
  • Prefer Task.Run over Task.Factory.StartNew .
  • Prefer async / await if you want to "return to the UI thread".
  • Use IProgress<T> for progress updates.
  • Only use TaskScheduler s or SynchronizationContext s if absolutely necessary (and they're not necessary here).
  • Never use Control.BeginInvoke or Control.Invoke .

In this case, your timer can just run NextValue on a thread pool thread using Task.Run , and then update the UI with the resulting value:

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;
}

For your progress bar, have your MyMethod take an 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);
  ...
}

Three things...

  1. Simplify your Thread Synchronization context for the progress bar. Just use Task.Run(() => { // progress bar code }
  2. Check that CPUCounter is not static
  3. Make sure your _CPUCounter.NextValue() method supports async and is awaited, otherwise it will block.

I would also favor Task.Run over Task.Factory.StartNew as it has better defaults (it calls Task.Factory.StartNew internally)

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);
        }

Have a look at the interval you're using. 50ms is a tad bit fast for the overhead of TPL.

Tried your code (just replaced the gauge with a simple label showing the usage variable and of course didn't click the button because you didn't provide MyMethod ) and didn't experience any UI blocking.

Posting the correct code for updating the UI (using Task for that purpose even with UI scheduler IMO is a overkill):

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;
        }));
    });
}

Still, there is no visible reason in your code that can cause UI blocking. Except eventually this part

_CPUCounter = new PerformanceCounter();
_CPUCounter.CategoryName = "Processor";
_CPUCounter.CounterName = "% Processor Time";
_CPUCounter.InstanceName = "_Total";

which runs on the UI thread in your OnFormLoad . You can try moving that code on a separate task like this

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();
        }));
    });
}

Somebody has already investigated this problem

The problem is that PerformanceCounter initialization is complex and takes a lot of time. What is not clear though is that whether this initialization block all threads or just the owning thread(the thread that creates the PerformanceCounter).

But I think your code already provided the answer. Since you are already creating the PerformanceCounter in a thread pool thread and it still blocks the UI. Then it is probably correct to assume the initialization blocks all threads.

If the initialization blocks all threads, then the solution would be to initialize the PerformanceCounter on application startup. Either by not using the default constructor(pick a constructor that not only constructs the instance but also initializes it) or call NextValue once during startup.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM