6

To Show a timer for how Long a specific process runs I'm using a Background worker to update an execution time Label. Naturally this should be done every second so that the user sees that it increases consistently.

After trying around a bit and failing utterly I went down the road that I'm checking every 150 milliseconds if the next second is already there and then I update the Display.

    private void ExecutionTimerBackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        Stopwatch executionTime = new Stopwatch();
        double lastUpdateAtSeconds = 0;

        executionTime.Start();

        while (!ExecutionTimerBackgroundWorker.CancellationPending)
        {
            Thread.Sleep(150);  // Sleep for some while to give other threads time to do their stuff

            if (executionTime.Elapsed.TotalSeconds > lastUpdateAtSeconds + 1)   // Update the Execution time display only once per second
            {
                ExecutionTimerBackgroundWorker.ReportProgress(0, executionTime.Elapsed);    // Update the Execution time Display
                lastUpdateAtSeconds  = executionTime.Elapsed.TotalSeconds;
            }
        }

        executionTime.Stop();
    }

    private void ExecutionTimerBackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // Update the display to the execution time in Minutes:Seconds format
        ExecutionTimeLabel.Text = ((TimeSpan) e.UserState).ToString(@"mm\:ss");    
    }

Now this seems to me a bit inefficient as I run it every 150 milliseconds to just look "hey has the next second already arrived or not". I also tried a different Approach already where I calculate how much time is needed until the next second but at that one I had a few instances where a jump by 2 instead of 1 second each in the Display happened.

So my question here is: Is there any more efficient way to do this? Or is that already how it should be done?

Thomas
  • 2,886
  • 3
  • 34
  • 78
  • Your if statement is executed every time round. That's because you neglected to set `lastUpdateAtSeconds` to the current value of `executionTime.Elapsed.TotalSeconds` – Jens Meinecke Jun 01 '15 at 10:10
  • Thanks corrected it was a copy & paste error – Thomas Jun 01 '15 at 10:12
  • How about setting a timer with 1 second and update the label with `timer.Elapsed`? No need a costly `Sleep()`, then end the timer once the task completed. – Eric Jun 01 '15 at 10:20
  • @Eric you mean like Anton suggests in his answer? – Thomas Jun 01 '15 at 11:33

4 Answers4

2

I have found that if you want to display changes every second then you should attempt to make the changes every tenth of a second for it to appear continuous for the user - maybe even more often than that.

Now I would avoid the use of a background worker entirely for this. Instead I'd use Microsoft's Reactive Framework (NuGet "Rx-Main" or "Rx-WinForms" in your case).

Here's the basic code for that:

var start = DateTimeOffset.Now;
var subscription =
    Observable
        .Interval(TimeSpan.FromSeconds(0.1))
        .Select(x => DateTimeOffset.Now.Subtract(start).TotalSeconds)
        .Select(x => (int)x)
        .DistinctUntilChanged()
        .ObserveOn(this)
        .Subscribe(x => this.label1.Text = x.ToString());

This code creates a timer (.Interval(...)) that fires every tenth of a second. It then computes the time in seconds since the code started, turns this into an integer, and drops all consecutive values that are the same. Finally it observes the observable on the UI thread (.ObserveOn(this)) and then subscribes by assigning the value to (in my case) a label on my form - you could use whatever control type you liked.

To stop the subscription, just do this:

subscription.Dispose();

It will clean up everything properly.

The code should be quite readable event if you are not familiar with the Reactive Framework.

Now I've used DateTimeOffset instead of Stopwatch as you don't need high resolution timing for updates occurring every second. Nothing would stop you using a Stopwatch if you wanted.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
1

For that matter I would suggest using System.Windows.Forms.Timer. With this timer you will not run into cross-thread issues when updating Label text and it is very easy to use.

private Timer timer;
private int secondsElapsed;

private void InitTimer()
{
    timer = new Timer();
    timer.Interval = 1000; // milliseconds
    timer.Tick += new EventHandler(timer_Tick);
}

void timer_Tick(object sender, EventArgs e)
{
    secondsElapsed++;
    lblSecondsElapsed.Text = secondsElapsed.ToString();
}

private void btnStart_Click(object sender, EventArgs e)
{
    secondsElapsed = 0;
    timer.Start();
}

private void btnAbort_Click(object sender, EventArgs e)
{
    timer.Stop();
}

Edit:

Source: MSDN - Timer Class (System.Windows.Forms)

This timer is optimized for use in Windows Forms applications and must be used in a window.

Note

The Windows Forms Timer component is single-threaded, and is limited to an accuracy of 55 milliseconds. If you require a multithreaded timer with greater accuracy, use the Timer class in the System.Timers namespace.

Anton Kedrov
  • 1,767
  • 2
  • 13
  • 20
  • How reliable is an update with it? (from what I saw from another answer the answerer there mentioned that he/she thinks it is not 100% reliable) – Thomas Jun 01 '15 at 11:30
  • It is limited to an accuracy of 55 milliseconds. – Anton Kedrov Jun 01 '15 at 12:00
  • I think that delay under 100 ms should be unnoticeable. Here is a good answers regarding this topic: http://stackoverflow.com/questions/536300/what-is-the-shortest-perceivable-application-response-delay – Anton Kedrov Jun 01 '15 at 12:20
  • +1 - multiple threads is overkill for displaying an execution timer: System.Windows.Forms.Timer is a much better solution. – Joe Jun 01 '15 at 12:31
0

You might want to take a look at System.Threading.Timer or System.Timers.Timer, but to be honest, even if you set the interval to 1 second, both are not very precise :/ most of the time, i go with 990ms or i use a thread much like you use your BackgroundWorker (and i don't think those timers really work any different).

Edit: Funny enough, i just looked in the .NET Framework and the Timers.Timer internally uses the Threading.Timer.

  • What do you mean with not very precise? Are they +-100ms there? Or do you mean something else? – Thomas Jun 01 '15 at 11:32
  • Going with a timer at 990ms is a bit weird. It means that every 100 seconds the timer will appear to freeze for 1980ms (990 x 2). – Enigmativity Jun 01 '15 at 12:00
  • @Enigmativity that's true to some point, but not entirely correct. because most of the time when i use such a timer it is off by around 10ms. don't ask me why, i can't put my finger on it. If i need a more precise "Timer", i either go with the Threading.Timer and an interval of 25ms checking myself when one second has passes or i directly use a thread with an internal sleep of 25ms (which isn't really "much"... you could even use 1ms interval without any "feelable" influence to the CPU) – Yasmin Endusa Jun 01 '15 at 16:30
0

You are totally right in your findings regarding the needed sampling interval for fluent changes of the displayed second. A human observer does know when the next second is due to arrive and hence notices even slight skews between this and the moment it actually is displayed. Thus you really need to make your check well below the sub-second level.

All considerations around efficiency should be centered around the question wether your check is computationally expensive or not. In your example above, the time spent in the if-clause is negligible, so the thread is quickly going to sleep again anyway. Thus you could safely afford small sleeping intervals 'just' to make display fluent.

If, however, your check would be computationally expensive, you could opt for adaptive sleeping: Sleep long if next second is far away, short when it is due soon...

Please be arware of the fact that, as Anton Kedrov stated, updating GUI components from any other than the GUI thread does throw an exeption. Depending on the grade of artificiality of your example, you should either opt for a completely timer-based solution, or insert a decoupling System.Windows.Forms.Timer to shovel the calculated values (via thread safe fields, please) to the GUI.

Peter Brennan
  • 1,366
  • 12
  • 28