3

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);
            }
        }
    }
}
svick
  • 236,525
  • 50
  • 385
  • 514
Valentina
  • 39
  • 6
  • Might be unrelated, but you should not update the UI element (gauge in your case) from the non UI thread. For the sake of test, what happens of you comment out the gauge code, i.e. leave only `float usage = _CPUCounter.NextValue();` line inside? – Ivan Stoev Feb 18 '16 at 20:49
  • What is the scope of _CPUCounter? Is it static? – Chris Ballance Feb 18 '16 at 20:54
  • @IvanStoev the UI is freezing anyway. I simply left that line of code. BTW, What do you suggest me to update the UI? For progress bar For button, on `Click` event, I call a method which starts a Task: `MyMethod((valuePBar) => UpdateProgressBar(valuePBar));` then `private void UpdateProgressBar(Int32 valuePBar) { Task.Factory.StartNew(() => { this.progressBar.Value = valuePBar; }, CancellationToken.None, TaskCreationOptions.None, this.ui_TaskScheduler); }` – Valentina Feb 18 '16 at 21:12
  • @ChrisBallance it is a public variable. – Valentina Feb 18 '16 at 21:15
  • Whaw! Why tasks, `Control.Invoke/BeginInvoke` is the WinForms standard way to update UI from another thread. What about the UI freezing, looks like it's from another place, the posted code cannot freeze the UI. Can you post small, but full sample code that we can copy/paste and see the issue? – Ivan Stoev Feb 18 '16 at 21:18
  • This article by Jon Skeet may help you out http://www.yoda.arachsys.com/csharp/threads/winforms.shtml – Chris Ballance Feb 18 '16 at 21:21
  • @IvanStoev so maybe I am doing everything wrong.. The code I posted before was for the progress bar. It is not causing the freezing. Since you told that I was updating the UI in the wrong way, I posted that piece of code to show you how I am doing stuff, and to know from you if it's wrong. I had taken that piece of code from a demo in CodeProject. – Valentina Feb 18 '16 at 21:22
  • The interval you're running this at might be a bit low for the overhead of TPL here. Try increasing your interval to say 200ms. – Chris Ballance Feb 18 '16 at 21:24
  • Thanks for your feedbacks. I am trying to do something new and would prefer to avoid old paradigms. Do you think it is possible to do something very simple using TPL? – Valentina Feb 18 '16 at 21:26
  • 3
    @Valentina I meant the code inside your original post (`OnTimerTickElapsed`) cannot freeze the UI. We need [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) – Ivan Stoev Feb 18 '16 at 21:27
  • @ChrisBallance same result with 500ms – Valentina Feb 18 '16 at 21:27
  • @IvanStoev I can guarantee that it is freezing :( I just posted my code example Thanks again! – Valentina Feb 18 '16 at 21:43

4 Answers4

3

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 TaskSchedulers or SynchronizationContexts 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);
  ...
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • thanks for your suggestion. Unfortunately, using `await` keyword causes compile error, maybe due to the fact that NextValue function has no `async` modifier. – Valentina Feb 19 '16 at 14:29
  • I don't understand the IProgress part. MyMethod is the one calculating the value to update the progressBar. – Valentina Feb 19 '16 at 14:43
  • @Valentina: `NextValue` doesn't have to be `async`. The compiler error you're seeing is probably because you forgot to make `OnTimerTickElapsed` `async`. The `IProgress` is a way of *reporting* progress updates; `MyMethod` still calculates the progress update value just like it did before. – Stephen Cleary Feb 19 '16 at 14:55
  • Got it. I made the `OnTimerTickElapsed async` but nothing has changed. Again, the problem has not been solved. Let's try to ignore the progress bar (thank for your suggestions, BTW). Did you succeed in having a fully responsive Ui with the `PerformanceCounter`? – Valentina Feb 19 '16 at 15:43
  • @Valentina: `NextValue` cannot possibly block the UI with this code. If your UI is blocking, it's some other code that's doing it. Please post a minimal, reproducible example. – Stephen Cleary Feb 19 '16 at 15:58
  • I have already put all my code here. The thing is: If I replace the performancecounter with a stupid random number generator, all is working. Why couldn;t it be the performance counter? – Valentina Feb 19 '16 at 16:47
  • @Valentina: It can't be `NextValue` because that method does not run on the UI thread. A "minimal, reproducible example" is the smallest amount of code that someone else can copy/paste into an application, and have it reproduce the problem. Your current code does not define `_CPUCounter` or `ultraGauge`. – Stephen Cleary Feb 19 '16 at 17:50
  • I simplified the code. I put only one progress bar, one gauge and one combobox with 3 ABC fields How can I post the code? it is too long for the admitted length of a comment. Anyway, the siomplified code still freezed, NOT ALWAYS, but sometime freezes – Valentina Feb 19 '16 at 19:29
  • @Valentina: Try replacing the gauge with a textbox or something, since random Internet people are likely not going to have the same gauge control you're using. Then edit your original question. – Stephen Cleary Feb 19 '16 at 19:44
1

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.

Chris Ballance
  • 33,810
  • 26
  • 104
  • 151
1

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();
        }));
    });
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Sorry I forgot MyMethod ` public static void MyMethod(Action CallBack) { Task.Factory.StartNew( () => { Thread.Sleep(1000); CallBack(20); Thread.Sleep(1000); CallBack(40); Thread.Sleep(1000); updateUICallBack(60); Thread.Sleep(1000); CallBack(80); Thread.Sleep(1000); CallBack(100); }); }` – Valentina Feb 19 '16 at 14:18
  • Please try to add myMethod to your code. I am still experiencing the freezing. I am pretty sure that the gauge is causing the problem. If I don't start the timer, everything is ok – Valentina Feb 19 '16 at 14:56
1

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.

Community
  • 1
  • 1
Xiaoguo Ge
  • 2,177
  • 20
  • 26
  • 1
    I ran into the same issue today. It seems only the current thread is blocked: wrapping the initialization of the performance counter inside a Task.Run() solves the problem. – Melvyn Aug 12 '16 at 14:57