如何在C# 4.0中使任务进入休眠状态(或延迟执行)?

75

.NET 4.0中如何实现与.NET 4.5中的Task.Delay相同的效果?


2
Thread.Sleep? - default
可能是重复问题:https://dev59.com/V2445IYBdhLWcg3wM3bx/ - default
4
使用Sleep()函数时,任务应该满足以下要求:1)必须生成一个单独的线程;2)只能有一个线程,不能多也不能少;3)不能被重复使用。而这些条件都不适用于一个任务。它不是一个副本,你提供的链接是关于延迟任务启动的,而我的问题是关于在任务开始后的任何时刻使其进入睡眠状态。 - Fulproof
我有点困惑,Task.Delay() 并不是让一个任务进入睡眠状态,而是创建了一个新的 Task。如果你等待它完成,那么它会让一个任务在指定的时间内进入睡眠状态。这是你要找的吗?一个在指定时间后完成的 Task - svick
12
在你的代码中使用 Thread.Sleep 几乎总是一个错误。 - Eli Arbel
显示剩余5条评论
9个回答

78

1
重要的信息是类名为 TaskEx 而不是 Task - OneWorld
这应该是答案。 - moien

62

你可以使用 Timer 在4.0中创建一个 Delay 方法:

public static Task Delay(double milliseconds)
{
    var tcs = new TaskCompletionSource<bool>();
    System.Timers.Timer timer = new System.Timers.Timer();
    timer.Elapsed+=(obj, args) =>
    {
        tcs.TrySetResult(true);
    };
    timer.Interval = milliseconds;
    timer.AutoReset = false;
    timer.Start();
    return tcs.Task;
}

1
如何将cancellationToken添加到Task.Delay提供的方法中? - Imran Rizvi
@ImranRizvi 添加一个什么都不做的继续任务,并将取消令牌传递给该继续任务。当然,如果您想要的话,也可以修改此方法以接受取消令牌,并在令牌取消事件触发时取消TCS,如果你有能力进行此修改的话。 - Servy
2
这会不会造成竞态条件?如果计时器对象在计时器到期之前被垃圾回收,似乎 TrySetResult 永远不会被调用。 - Edward Brey
10
Timer类在内部专门处理这个问题,以确保使用它的用户不需要在其生命周期中持有对它的引用。只要计时器正在运行,它就会从根位置添加对自身的引用,然后在不再运行时将其删除。 - Servy
@AdamRobinson 这完全取决于上下文。这取决于对象是什么,未托管的资源是什么,不清理它的后果是什么,以及使用它的程序的具体情况。有些情况比其他情况更重要,但通常最好清理它们。 - Servy
显示剩余6条评论

26
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Delay(2000).ContinueWith(_ => Console.WriteLine("Done"));
        Console.Read();
    }

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

来自《如何在4.0中实现Task.Delay》一节。


3
如果链接由于某些原因失效,我已经在你的回答中添加了示例。 - default
2
@Fulproof使用LinqPad编写了这个程序,它添加了一个扩展方法到object,用于打印其ToString方法的值。请注意,他在实际实现中没有使用该方法或任何其他非库方法,只是测试它的示例函数。 - Servy
@Servy,谢谢。我提出问题是为了减少未知数的数量(并获得答案),而不是增加需要解决的难题。也就是说,通常提问的人没有完成难题所需的专业知识。 - Fulproof
1
已更新,因此您可以直接复制粘贴并运行) - QrystaL
计时器不应该被清除吗? - Amit G

6
以下是可取消的Task.Delay实现的代码和示例测试。您可能对Delay方法感兴趣。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DelayImplementation
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Threading.CancellationTokenSource tcs = new System.Threading.CancellationTokenSource();

            int id = 1;
            Console.WriteLine(string.Format("Starting new delay task {0}. This one will be cancelled.", id));
            Task delayTask = Delay(8000, tcs.Token);
            HandleTask(delayTask, id);

            System.Threading.Thread.Sleep(2000);
            tcs.Cancel();

            id = 2;
            System.Threading.CancellationTokenSource tcs2 = new System.Threading.CancellationTokenSource();
            Console.WriteLine(string.Format("Starting delay task {0}. This one will NOT be cancelled.", id));
            var delayTask2 = Delay(4000, tcs2.Token);
            HandleTask(delayTask2, id);

            System.Console.ReadLine();
        }

        private static void HandleTask(Task delayTask, int id)
        {
            delayTask.ContinueWith(p => Console.WriteLine(string.Format("Task {0} was cancelled.", id)), TaskContinuationOptions.OnlyOnCanceled);
            delayTask.ContinueWith(p => Console.WriteLine(string.Format("Task {0} was completed.", id)), TaskContinuationOptions.OnlyOnRanToCompletion);
        }

        static Task Delay(int delayTime, System.Threading.CancellationToken token)
        {
            TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();

            if (delayTime < 0) throw new ArgumentOutOfRangeException("Delay time cannot be under 0");

            System.Threading.Timer timer = null;
            timer = new System.Threading.Timer(p =>
            {
                timer.Dispose(); //stop the timer
                tcs.TrySetResult(null); //timer expired, attempt to move task to the completed state.
            }, null, delayTime, System.Threading.Timeout.Infinite);

            token.Register(() =>
                {
                    timer.Dispose(); //stop the timer
                    tcs.TrySetCanceled(); //attempt to mode task to canceled state
                });

            return tcs.Task;
        }
    }
}

看起来不太好!如果我使用相同的CancellationToken调用了很多延迟,那么会在令牌上注册很多委托。 - RollingStone

1
扩展这个 答案 的想法:
new AutoResetEvent(false).WaitOne(1000);

0
    public static void DelayExecute(double delay, Action actionToExecute)
    {
        if (actionToExecute != null)
        {
            var timer = new DispatcherTimer
            {
                Interval = TimeSpan.FromMilliseconds(delay)
            };
            timer.Tick += (delegate
            {
                timer.Stop();
                actionToExecute();
            });
            timer.Start();
        }
    }

0

5
这不是一个好主意,CTP包含已知的永远无法修复的漏洞。使用Bcl.Async是更好的选择。 - svick

0
在许多情况下,一个简单的AutoResetEvent比Thread.Sleep()更好...
AutoResetEvent pause = new AutoResetEvent(false);
Task timeout = Task.Factory.StartNew(()=>{
pause.WaitOne(1000, true);
});

希望它有所帮助。

我认为最初的想法是阻止当前线程,而不是在另一个线程上等待并稍后在当前线程上收到通知。那么,这个 怎么样? - Alex R.

0
这是一个简洁、基于定时器的实现,带有适当的清理操作:
var wait = new TaskCompletionSource<bool>();
using (new Timer(_ => wait.SetResult(false), null, delay, Timeout.Infinite))
    await wait.Task;

如果要在.NET 4.0上使用此代码,您需要安装Microsoft.Bcl.Async NuGet包。


OP正在询问4.0版本 - 4.0版本中没有"await"。 - Alex R.
1
await 关键字是 C# 语言的一部分,与 .NET Framework 版本是分开的。问题在于 Task<T> 没有 GetAwaiter,而 await 依赖于它。Microsoft.Bcl.Async 提供了这个功能。我更新了我的答案,提到了这个 NuGet 包。 - Edward Brey
2
如果要使用Microsoft.Bcl.Async,那么TaskEx.Delay更加简洁,是吗? - Alex R.

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