I have run into a problem which is (to me) rather odd.
I have a C#/.net application in which I use a stopwatch to get accurate timing on an event. During the timing, I want to update a label with the progress. My code (simplified):
private void backgroundWorker3_DoWork(object sender, DoWorkEventArgs e)
{
double stepDuration = Convert.ToDouble(textBoxPenetrationTime.Text);
Stopwatch clock = new Stopwatch();
long freq = Stopwatch.Frequency;
double ticksPerStep = (double)freq * stepDuration;
long preTick, postTick;
clock.Restart();
preTick = clock.ElapsedTicks;
while (clock.ElapsedTicks < ticksPerStep)
{
labelPercent.BeginInvoke((Action)delegate () { labelPercent.Text = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000)) + "%"; });
if (backgroundWorker3.CancellationPending == true)
{
return;
}
}
postTick = clock.ElapsedTicks;
}
What happens when I run start the backgroundworker is that the rest of the UI (dataplotting and labels handled in another backgroundworker freeze, as well as the entire form) hangs.
What I have tried:
Updating the label in another backgroundworker and call that backgroundworker inside the while loop (same result). Tried calling Application.DoEvents() after updating label (no difference)
One thing that works, which is the reason why I described the issue as odd, is that if I call
Console.WriteLine(clock.ElapsedTicks);
at the beginning of the while loop, everything runs smoothly as intended. I just discovered that by chance.
So my question has two parts:
You need to reduce the number of times you schedule work on the UI thread. Every time you call Invoke you ask for an update of the UI. If you have a tight loop you can be calling Invoke easily more than 60 times per second. Unless you're a gamer and/or Linus from LTT you won't see / notice all updates of your label, even if it would output different values on each call.
When you added a call to Console.WriteLine you basically reduced the load on the UI thread enough to become responsive again. That is because Console.WriteLine is a call that is relatively expensive when it comes to CPU cycles / IO load. That comes at the price that your background task now takes longer to complete.
I suggest the following changes:
The implementation of the ProgressChanged event
backgroundWorker3.ProgressChanged += (sender, progress) => {
labelPercent.Text = progress.ProgressPercentage.ToString("#0 \\%");
};
Here is what your DoWork would look like when keeping track of the last percentage so you can call ReportProgess when there is reason to do so.
double stepDuration = Convert.ToDouble(10);
Stopwatch clock = new Stopwatch();
long freq = Stopwatch.Frequency;
double ticksPerStep = (double)freq * stepDuration;
long preTick, postTick;
clock.Restart();
preTick = clock.ElapsedTicks;
var lastPercentage = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000));
bgw.ReportProgress(lastPercentage);
while (clock.ElapsedTicks < ticksPerStep)
{
var nextPercentage = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000));
if (nextPercentage != lastPercentage) {
bgw.ReportProgress(nextPercentage);
lastPercentage = nextPercentage;
}
// do work
}
bgw.ReportProgress(100);
Here is what your DoWork would look like if you used a Timer to update the label 25 times per second. This comes at the cost of an extra thread and more calls to the UI thread but your worker loop is free of progress concerns.
double stepDuration = Convert.ToDouble(10);
Stopwatch clock = new Stopwatch();
long freq = Stopwatch.Frequency;
double ticksPerStep = (double)freq * stepDuration;
long preTick, postTick;
using(var timer = new System.Threading.Timer(
(e) => {
bgw.ReportProgress(Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000)));
}, null, 40, 40))
{
clock.Restart();
preTick = clock.ElapsedTicks;
while (clock.ElapsedTicks < ticksPerStep)
{
// do work
}
}
bgw.ReportProgress(100);
Here is a Gif of the timer solution in action: