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

2

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);
Barbwire answered 28/11, 2013 at 8:42 Comment(2)
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.Scholl
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<Task> object returned by Task.Factory.StartNew.Scholl
S
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();
        }
    }
}
Scholl answered 28/11, 2013 at 11:24 Comment(13)
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>(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.Barbwire
@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.Scholl
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<Task>(Func<Task>, CancellationToken, TaskCreationOptions, TaskScheduler)'. Everything I read indicates that this needs to be passed as a lambda or delegate.Barbwire
@JoshuaBarker, try this: TaskFactory.StartNew(new Func<Task>(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.Scholl
So, I ended up creating two variables, one for Task<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...Barbwire
@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.Scholl
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: forums.xamarin.com/discussion/10858/…Barbwire
@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: msdn.microsoft.com/en-us/magazine/gg598924.aspxScholl
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<bool> 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?Barbwire
@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.Scholl
Ok... thanks for the clarification. It looks like this may be a bug in Xamarin's implementation of Tasks (see forums.xamarin.com/discussion/10858/… and 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!Barbwire
_task.Wait() in Stop(). That helped me greatly!Theriot
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.Teleran
C
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);
}
Crum answered 14/3, 2022 at 9:22 Comment(3)
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.Kimmy
in case "_taskRunner.Handle(cancellationToken)" is long running operation, checking if the task canceled enable to stop the parent task before it startCrum
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.Kimmy

© 2022 - 2024 — McMap. All rights reserved.