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);
Async void methods can wreak havoc if the caller isn't expecting them to be async. When the return type is Task, the caller knows it's dealing with a future operation; when the return type is void, the caller might assume the method is complete by the time it returns.
There is no parallelism here, as the “async Task” does not automatically make something run in in parallel. This will spawn 2 threads, run them simultaneously, and return when both threads are done. This will create a list of Tasks to be run at the same time.
In computer programming, the async/await pattern is a syntactic feature of many programming languages that allows an asynchronous, non-blocking function to be structured in a way similar to an ordinary synchronous function.
The ContinueWith function is a method available on the task that allows executing code after the task has finished execution. In simple words it allows continuation. Things to note here is that ContinueWith also returns one Task. That means you can attach ContinueWith one task returned by this method.
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(); } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With