Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is correct way to combine long-running tasks with async / await pattern?

Tags:

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); 
like image 287
Joshua Barker Avatar asked Nov 28 '13 08:11

Joshua Barker


People also ask

Why you shouldn't use async void?

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.

Do async tasks run in parallel?

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.

What is the async await pattern?

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.

When should I use Task ContinueWith?

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.


1 Answers

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();         }     } } 
like image 119
noseratio Avatar answered Sep 19 '22 21:09

noseratio