26

I have a "High-Precision" timer class that I need to be able to be start, stop & pause / resume. To do this, I'm tying together a couple of different examples I found on the internet, but I'm not sure if I'm using Tasks with asnyc / await correctly.

Here is my relevant code:

//based on http://haukcode.wordpress.com/2013/01/29/high-precision-timer-in-netc/
public class HighPrecisionTimer : IDisposable
{
    Task _task;
    CancellationTokenSource _cancelSource;

    //based on http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperatively-pausing-async-methods.aspx
    PauseTokenSource _pauseSource;

    Stopwatch _watch;
    Stopwatch Watch { get { return _watch ?? (_watch = Stopwatch.StartNew()); } }

    public bool IsPaused
    {
        get { return _pauseSource != null && _pauseSource.IsPaused; }
        private set
        {
            if (value)
            {
                _pauseSource = new PauseTokenSource();
            }
            else
            {
                _pauseSource.IsPaused = false;
            }
        }
    }

    public bool IsRunning { get { return !IsPaused && _task != null && _task.Status == TaskStatus.Running; } }

    public void Start()
    {
        if (IsPaused)
        {
            IsPaused = false;
        }
        else if (!IsRunning)
        {
            _cancelSource = new CancellationTokenSource();
            _task = new Task(ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning);
            _task.Start();
        }
    }

    public void Stop()
    {
        if (_cancelSource != null)
        {
            _cancelSource.Cancel();
        }
    }

    public void Pause()
    {
        if (!IsPaused)
        {
            if (_watch != null)
            {
                _watch.Stop();
            }
        }

        IsPaused = !IsPaused;
    }

    async void ExecuteAsync()
    {
        while (!_cancelSource.IsCancellationRequested)
        {
            if (_pauseSource != null && _pauseSource.IsPaused)
            {
                await _pauseSource.Token.WaitWhilePausedAsync();
            }

            // DO CUSTOM TIMER STUFF...
        }

        if (_watch != null)
        {
            _watch.Stop();
            _watch = null;
        }

        _cancelSource = null;
        _pauseSource = null;
    }

    public void Dispose()
    {
        if (IsRunning)
        {
            _cancelSource.Cancel();
        }
    }
}

Can anyone please take a look and provide me some pointers on whether I'm doing this correctly?

UPDATE

I have tried modifying my code per Noseratio's comments below, but I still cannot figure out the syntax. Every attempt to pass the ExecuteAsync() method to either TaskFactory.StartNew or Task.Run, results in a compilation error like the following:

"The call is ambiguous between the following methods or properties: TaskFactory.StartNew(Action, CancellationToken...) and TaskFactory.StartNew<Task>(Func<Task>, CancellationToken...)".

Finally, is there a way to specify the LongRunning TaskCreationOption without having to provide a TaskScheduler?

async **Task** ExecuteAsync()
{
    while (!_cancelSource.IsCancellationRequested)
    {
        if (_pauseSource != null && _pauseSource.IsPaused)
        {
            await _pauseSource.Token.WaitWhilePausedAsync();
        }
        //...
    }
}

public void Start()
{
    //_task = Task.Factory.StartNew(ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning, null);

    //_task = Task.Factory.StartNew(ExecuteAsync, _cancelSource.Token);

    //_task = Task.Run(ExecuteAsync, _cancelSource.Token);

}

UPDATE 2

I think I've narrowed this down, but still not sure about the correct syntax. Would this be the right way to create the task so that the consumer / calling code continues on, with the task spinning-up and starting on a new asynchronous thread?

_task = Task.Run(async () => await ExecuteAsync, _cancelSource.Token);

//**OR**

_task = Task.Factory.StartNew(async () => await ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Joshua Barker
  • 987
  • 2
  • 11
  • 23
  • Passing `async () => await ExecuteAsync` lambda to `Task.Factory.StartNew` doesn't solve the problem I've described in #1. Essentially, it changes nothings. Check out an update to my answer for an example of correct syntax and logic. – noseratio Nov 30 '13 at 00:33
  • Actually, now that you've changed the return type of `ExecuteAsync` to `Task`, passing `async () => await ExecuteAsync` lambda to `Task.Factory.StartNew` would work, but it's redundant. Just pass `ExecuteAsync` and do `task.Unwrap` on the `Task` object returned by `Task.Factory.StartNew`. – noseratio Dec 02 '13 at 00:46

2 Answers2

25

Here are some points:

  • async void methods are only good for asynchronous event handlers (more info). Your async void ExecuteAsync() returns instantly (as soon as the code flow reaches await _pauseSource inside it). Essentially, your _task is in the completed state after that, while the rest of ExecuteAsync will be executed unobserved (because it's void). It may even not continue executing at all, depending on when your main thread (and thus, the process) terminates.

  • Given that, you should make it async Task ExecuteAsync(), and use Task.Run or Task.Factory.StartNew instead of new Task to start it. Because you want your task's action method be async, you'd be dealing with nested tasks here, i.e. Task<Task>, which Task.Run would automatically unwrap for you. More info can be found here and here.

  • PauseTokenSource takes the following approach (by design, AFAIU): the consumer side of the code (the one which calls Pause) actually only requests a pause, but doesn't synchronize on it. It will continue executing after Pause, even though the producer side may not have reached the awaiting state yet, i.e. await _pauseSource.Token.WaitWhilePausedAsync(). This may be ok for your app logic, but you should be aware of it. More info here.

[UPDATE] Below is the correct syntax for using Factory.StartNew. Note Task<Task> and task.Unwrap. Also note _task.Wait() in Stop, it's there to make sure the task has completed when Stop returns (in a way similar to Thread.Join). Also, TaskScheduler.Default is used to instruct Factory.StartNew to use the thread pool scheduler. This is important if your create your HighPrecisionTimer object from inside another task, which in turn was created on a thread with non-default synchronization context, e.g. a UI thread (more info here and here).

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    public class HighPrecisionTimer
    {
        Task _task;
        CancellationTokenSource _cancelSource;

        public void Start()
        {
            _cancelSource = new CancellationTokenSource();

            Task<Task> task = Task.Factory.StartNew(
                function: ExecuteAsync, 
                cancellationToken: _cancelSource.Token, 
                creationOptions: TaskCreationOptions.LongRunning, 
                scheduler: TaskScheduler.Default);

            _task = task.Unwrap();
        }

        public void Stop()
        {
            _cancelSource.Cancel(); // request the cancellation

            _task.Wait(); // wait for the task to complete
        }

        async Task ExecuteAsync()
        {
            Console.WriteLine("Enter ExecuteAsync");
            while (!_cancelSource.IsCancellationRequested)
            {
                await Task.Delay(42); // for testing

                // DO CUSTOM TIMER STUFF...
            }
            Console.WriteLine("Exit ExecuteAsync");
        }
    }

    class Program
    {
        public static void Main()
        {
            var highPrecisionTimer = new HighPrecisionTimer();

            Console.WriteLine("Start timer");
            highPrecisionTimer.Start();

            Thread.Sleep(2000);

            Console.WriteLine("Stop timer");
            highPrecisionTimer.Stop();

            Console.WriteLine("Press Enter to exit...");
            Console.ReadLine();
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thanks for the info on #1 above, did not realize that... Regarding #2, I actually tried using both Task.Factory.StartNew() and Task.Run, but could not figure out the correct syntax for passing my async Task ExecuteAsync() method, I keep getting compile errors, either about the call being ambiguous, or delegate does not match Action(object). Additionally, I'm using MonoTouch, which adds an extra layer on top of .Net, so I don't know if its a problem with my code, or with the MonoTouch framework. – Joshua Barker Nov 28 '13 at 20:32
  • 1
    @JoshuaBarker, I've updated my answer with some code showing the correct syntax for `Task.Factory.StartNew`, along with some more thoughs. Everything should apply to Mono runtime environment, too. I believe they follow the .NET specs pretty closely. – noseratio Nov 29 '13 at 00:49
  • Hi Noseratio... Thanks for the update.. Unfortunately, I cannot pass the ExecuteAsync method directly as the function parameter to either Task.Factory.StartNew() or Task.Run(). As I stated above, I get a the following compilation error: Error CS0121: The call is ambiguous between the following methods or properties: 'TaskFactory.StartNew(Action, CancellationToken, TaskCreationOptions, TaskScheduler)' and 'TaskFactory.StartNew(Func, CancellationToken, TaskCreationOptions, TaskScheduler)'. Everything I read indicates that this needs to be passed as a lambda or delegate. – Joshua Barker Dec 02 '13 at 06:29
  • @JoshuaBarker, try this: `TaskFactory.StartNew(new Func(ExecuteAsync), ...`, it will eliminate the ambiguity. Don't forget to do `task.Unwrap()`. Anyhow, I can't tell why you're seeing this error, while I'm not when I compile the code I posted. – noseratio Dec 02 '13 at 06:34
  • 1
    So, I ended up creating two variables, one for Task and the other for just Task. Interesting note, but when I examine both variables in the debug window, its the "outer" Task wrapper that has its status as "Running", whereas the inner Task is always "WaitingForActiviation"... So calling pause now works, in that I can pause / resume my running thread, but something weird in iOS or MonoTouch causes that thread to move from a background thread (#5) to the main UI thread (#1)... Two steps forward, 1 step backwards... – Joshua Barker Dec 02 '13 at 06:53
  • @JoshuaBarker, before you do `IsPaused = true|false`, check the current thread's synchronization context: `System.Threading.SynchronizationContext.Current`, what is it? It's possible for the control flow inside your task to switch pool threads, but it should never jump on the UI thread (which has a dedicated synchronization context). Could be a bug in Mono/MonoTouch. – noseratio Dec 02 '13 at 08:49
  • Ok, so I'm checking the SynchronizationContext... but not sure what I should be looking for... They appear to be the same object both before and after pausing/un-pausing, but I'm still seeing the thread being changed... see the following for more details: http://forums.xamarin.com/discussion/10858/async-await-finishes-running-thread-and-resumes-execution-on-main-ui-thread#latest – Joshua Barker Dec 02 '13 at 20:50
  • @JoshuaBarker, check the type of the object: `System.Threading.SynchronizationContext.Current.GetType().ToString()`. I'm not really familiar with MonoTouch, but it should **not** be `SynchronizationContext` for the UI thread (where you create your `HighPrecisionTimer` object and request pause/resume). Check this out for the reasoning: http://msdn.microsoft.com/en-us/magazine/gg598924.aspx – noseratio Dec 02 '13 at 21:32
  • Ok, so it looks like the object is of type: MonoTouch.UIKit.UIKitSynchronizationContext. However, I think I may have found something. I added some additional logging to the PauseTokenSource class, and realized that when the IsPaused is being set to True, that this is occurring under the main UI thread (Thread #1), because that where the UI button click event originates from. Since the TaskCompletionSource is being created under the main UI thread, could this be why its returning to that thread on completion, even though the await is called from Thread #5? – Joshua Barker Dec 03 '13 at 23:13
  • @JoshuaBarker, that's actually the correct behavior, AFAUI. It doesn't matter on what thread you create `TaskCompletionSource`, but it matters on what thread (or rather on what synchronization context) you do `await TaskCompletionSource.Task`. So, you call `IsPaused` on the UI thread, and you stay on this thread. At the same time, your task (started with `StartNew`) runs on a pool thread and does `await TaskCompletionSource.Task` there. It should never jump on the UI thread, even if that's where `TaskCompletionSource.SetResult` is called from. – noseratio Dec 04 '13 at 00:25
  • 1
    Ok... thanks for the clarification. It looks like this may be a bug in Xamarin's implementation of Tasks (see http://forums.xamarin.com/discussion/10858/async-await-finishes-running-thread-and-resumes-execution-on-main-ui-thread#latest and https://bugzilla.xamarin.com/show_bug.cgi?id=16548 for more details). Hopefully they get this fixed ASAP. In the meantime, I appreciate all your help! – Joshua Barker Dec 04 '13 at 18:13
  • 1
    `_task.Wait()` in `Stop()`. That helped me greatly! – Dib Jun 11 '17 at 05:24
  • 5
    Note that this might not work as expected. The only `Task` in this example which is invoked as `LongRunning` is the one created by `Task.Factory.StartNew`. This _does_ cause the default scheduler to schedule that particular task on its own thread, but it doesn't `await` on `ExecuteAsync`, it just invokes it. So what happens is that `ExecuteAsync` runs on that thread until it encounters its first `await`, at which point the thread returns to executing the parent task and returns. When `ExecuteAsync` resumes, it resumes on a thread pool thread, thus negating the `LongRunning` flag. – Mike E Oct 30 '17 at 21:27
0

I'm adding code for running long running task (infinite with cancelation) with internal sub tasks:

Task StartLoop(CancellationToken cancellationToken)
{
   return Task.Factory.StartNew(async () => {
       while (true)
       {
           if (cancellationToken.IsCancellationRequested)
               break;
    
           await _taskRunner.Handle(cancellationToken);
           await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
       }
     },
     cancellationToken,
     TaskCreationOptions.LongRunning,
     TaskScheduler.Default);
}
Eli Dagan
  • 808
  • 8
  • 14
  • You can safely remove this line of code: `if (cancellationToken.IsCancellationRequested) break;`. In case of cancellation, the `Task.Delay` will throw an `OperationCanceledException` anyway. Including the `IsCancellationRequested` check will only contribute in your method having inconsistent behavior in case of cancellation. Also for maintaining a stable interval between invoking the handler, you could create the `Task.Delay` task before invoking the handler, and `await` it afterwards. – Theodor Zoulias Mar 14 '22 at 12:56
  • in case "_taskRunner.Handle(cancellationToken)" is long running operation, checking if the task canceled enable to stop the parent task before it start – Eli Dagan Apr 18 '23 at 07:23
  • You are passing the `cancellationToken` to the `Task.Factory.StartNew` method, so if the token is canceled from the beginning the task will complete immediately as canceled. If you are worried that you are leaving a time gap where the token is not checked, start with this: `cancellationToken.ThrowIfCancellationRequested();`, to get a consistent cancellation behavior. – Theodor Zoulias Apr 18 '23 at 10:41