简体   繁体   中英

Canceling Task Delay in .Net 4.0

I am currently trying to implement a substitute for .Net 4.5's Task.Delay() method in a program that must target .Net 4.0. I found the following code at this blog .

    /* You can write Task-based asynchronous methods by utilizing a TaskCompletionSource.
A TaskCompletionSource gives you a 'slave' Task that you can manually signal.
Calling SetResult() signals the task as complete, and any continuations kick off. */

void Main()
{    
    for (int i = 0; i < 10000; i++)
    {
        Task task = Delay (2000);
        task.ContinueWith (_ => "Done".Dump());
    }
}

Task Delay (int milliseconds)        // Asynchronous NON-BLOCKING method
{
    var tcs = new TaskCompletionSource<object>();
    new Timer (_ => tcs.SetResult (null)).Change (milliseconds, -1);
    return tcs.Task;
}

Tasks are fairly new to me. System.Threading.Timer and TaskCompletionSource are brand new to me (as of today), and I'm struggling a bit with them. All that aside, I'm wondering how I might add CancellationToken functionality to this code. I'm assuming I could add a parameter to the Delay() method like this:

Task Delay (int milliseconds, CancellationToken token)        // Asynchronous NON-BLOCKING method
{
    var tcs = new TaskCompletionSource<object>();
    new Timer (_ => tcs.SetResult (null)).Change (milliseconds, -1);
    return tcs.Task;
}

... but then, where do I put the logic for checking the token and getting out of the method? Somewhere in the callback? Is this even possible?

I've tried to change your code as little as possible but here is a working example that behaves in the same way as Task.Delay.

It's important to note that I use TrySetCanceled and TrySetResult because the Timer could finish after the task is canceled. Ideally you want to stop the timer.

Also note a canceled task will throw a TaskCanceledException

static void Main(string[] args)
{
    // A cancellation source that will cancel itself after 1 second
    var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));

    try
    {
        // This will only wait 1 second because as it will be cancelled.
        Task t = Delay(5000, cancellationTokenSource.Token);                
        t.Wait();
        Console.WriteLine("The task completed");
    }
    catch (AggregateException exception)
    {
        // Expecting a TaskCanceledException
        foreach (Exception ex in exception.InnerExceptions)
            Console.WriteLine("Exception: {0}", ex.Message);
    }
    Console.WriteLine("Done");
    Console.ReadLine();
}

private static Task Delay(int milliseconds, CancellationToken token)
{
    var tcs = new TaskCompletionSource<object>();
    token.Register(() => tcs.TrySetCanceled());
    Timer timer = new Timer(_ => tcs.TrySetResult(null));
    timer.Change(milliseconds, -1);            
    return tcs.Task;
}

Reading a bit more into your question. If you need Task.Delay and you're targeting .NET 4.0 then you should use the Microsoft Async nuget package from http://www.nuget.org/packages/Microsoft.Bcl.Async/ it contains the method TaskEx.Delay

这样

token.Register(() => tcs.TrySetCancelled());

Here you are a version that prevents disposal of timer by the garbage collector

    public static Task Delay(int milliseconds, CancellationToken token)
    {
        var tcs = new TaskCompletionSource<object>();
        var timer = new OneShotTimer((t) => {
            using ((OneShotTimer)t)
                tcs.SetResult(null);
        });
        token.Register(() => {
            if (timer.TryCancel())
            {
                using (timer)
                    tcs.SetCanceled();
            }
        });
        timer.Start(milliseconds);
        return tcs.Task;
    }


    public class OneShotTimer : IDisposable
    {
        private readonly object sync = new object();
        private readonly TimerCallback oneShotCallback;
        private readonly Timer timer;
        private bool isActive;

        public OneShotTimer(TimerCallback oneShotCallback, int dueTime = Timeout.Infinite)
        {
            this.oneShotCallback = oneShotCallback;
            this.isActive = dueTime != Timeout.Infinite;
            this.timer = new Timer(callback, this, dueTime, Timeout.Infinite);
        }


        public void Dispose()
        {
            timer.Dispose();
        }


        public void Start(int dueTime)
        {
            if (!tryChange(true, dueTime))
                throw new InvalidOperationException("The timer has already been started");
        }


        public bool TryCancel()
        {
            return tryChange(false, Timeout.Infinite);
        }


        public bool tryChange(bool targetIsActive, int dueTime)
        {
            bool result = false;
            lock (sync)
            {
                if (isActive != targetIsActive)
                {
                    result = true;
                    isActive = targetIsActive;
                    timer.Change(dueTime, Timeout.Infinite);
                }
            }
            return result;
        }


        private static void callback(object state)
        {
            var oneShotTimer = (OneShotTimer)state;
            if (oneShotTimer.TryCancel())
                oneShotTimer.oneShotCallback(oneShotTimer);
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM