在 .Net 4.0 中取消任务延迟

6

我目前正试图在一个必须针对.Net 4.0的程序中实现替代.Net 4.5的Task.Delay()方法。我在这个博客上找到了以下代码。

    /* 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;
}

任务对我来说还比较新。 System.Threading.TimerTaskCompletionSource 对我来说是全新的(截至今天),我对它们有些困惑。 除此之外,我想知道如何将CancellationToken功能添加到这段代码中。 我假设可以像这样向Delay()方法添加一个参数:

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;
}

...但是,我应该在哪里放置检查令牌并退出方法的逻辑呢?是在回调函数中吗?这是否可能?


3
Microsoft提供了一个NuGet包Microsoft.Bcl.Async,将许多4.5版本新增的功能回溯到4.0。如果您已安装此包,就可以通过TaskEx.Delay()来使用延迟功能。 - Scott Chamberlain
3个回答

4

我尽可能地少改动了你的代码,但是这里有一个可以工作的示例,其行为方式与Task.Delay相同。

需要注意的是,我使用了TrySetCanceledTrySetResult,因为计时器可能在任务被取消后完成。理想情况下,您希望停止计时器。

还要注意,已取消的任务将抛出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;
}

根据您的问题稍作了解,如果您需要使用Task.Delay并且目标平台为.NET 4.0,那么您应该使用Microsoft Async nuget包,下载链接见http://www.nuget.org/packages/Microsoft.Bcl.Async/,里面包含了方法TaskEx.Delay


2
这是一个很好的双重答案。我已经在我的项目中安装了Microsoft.Bcl.Async,但我没有意识到他们将那些方法隐藏在TaskEx下面。感谢您对上下文的详细解释和BCL扩展提示。 - bubbleking
在.NET 4.0中,无法使用超时初始化CancellationTokenSource。 - VivaLaPanda

2

就像这样

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

这个在什么上下文中使用?如果我看起来很明显,请原谅,因为这里有几个新概念对我来说。 - bubbleking

0

这里有一个版本,可以防止垃圾回收器对计时器进行处理

    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);
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接