替代Thread.Sleep()的方法

65
每隔N分钟我们希望运行一次任务列表。因此,我们创建了一个任务执行器,其中包含一个 scheduledExecutorService ,它将在计划的时间间隔内运行这些任务。
do { DoWork(); }while(!stopRequested)

现在我们想要在工作周期之间暂停。大家似乎都认为Thread.Sleep()是魔鬼。我看到有人提到使用Monitor/Event的东西,但我们没有别人告诉我们要做什么工作。我们只想每N分钟做一些事情,如钟表般准时。

所以是否有替代方案或者我已经找到了Thread.Sleep的有效用法?

另外一个帖子中的某人提到了WaitHandle.WaitOne()作为替代方案,但显然无法从非静态方法中调用它?或者至少我不能,因为我得到了编译时错误..

  

需要对象引用   ‘System.Threading.WaitHandle.WaitOne(System.TimeSpan)’的非静态字段、方法或属性

11个回答

102

据我所理解,Thread.Sleep()不好的原因是它会强制线程的资源从高速缓存中移除,之后必须重新加载。虽然这并不是很严重,但在高负载情况下可能会加剧性能问题。此外,还存在一个事实,那就是它的时间精度不够,并且无法等待低于约10毫秒的持续时间......

我使用以下代码片段:

new System.Threading.ManualResetEvent(false).WaitOne(1000);

非常简单,所有内容都放在一行上。创建一个新的事件处理程序,然后等待完整的超时时间,这个时间是作为WaitOne()的参数指定的。

不过,对于这种特定情况,计时器可能是更合适的方法:

var workTimer = new System.Threading.Timer(
    (x) => DoWork(),
    null,
    1000, // initial wait period
    300000); // subsequent wait period

那么,不需要设置取消变量,你可以使用workTimer.Stop()停止计时器。


编辑:

由于人们仍然发现这很有用,我应该补充一下,.NET 4.5引入了Task.Delay方法,它更加简洁,也支持异步操作:

Task.Delay(2000).Wait(); // Wait 2 seconds with blocking
await Task.Delay(2000); // Wait 2 seconds without blocking

2
我发现你的一行代码片段对我非常有用,当我在寻找替代 thread.sleep 时。我只想暂停一两秒钟,并且每次都能够记录到控制台,但是使用 thread.sleep 无法实现。 - Someone
2
我注意到ManualResetEventIDisposable的,但是你的代码让匿名实例被垃圾回收了。在这种情况下不显式调用Dispose是否存在任何风险? - Dai
2
好问题。从反汇编中我所看到的,ManualResetEvent使用了基本的WaitHandle功能,其Dispose方法只是处理其内部的SafeWaitHandle实例。SafeWaitHandle的基类SafeHandle在其析构函数中显式调用了自己的Dispose方法,因此我认为当对象被GC时,任何未管理的OS句柄都会在那个时候被清理掉。但是,在2016年最好的选择是使用Task.Delay并避免整个问题。 :) - Calvin Fisher
await Task.Delay(2000)会在高负载情况下准确地在2秒后执行吗?还是这取决于线程池中下一个线程的可用性?(该问题更偏向于ASP.NET,我不确定Task是否依赖于线程池。如果问题表述不清,请见谅。) - Krishnan

26

当然,你必须在WaitHandle上调用WaitOne实例方法。否则它怎么知道等待什么呢?

最好拥有某些可以触发反应的东西,而不是休眠,这样你就可以在没有任何原因等待几分钟的情况下注意到取消。另一种使用方式除了WaitHandle之外是使用Monitor.Wait/Pulse

但是,如果您使用.NET 4,我建议您研究一下任务并行库(Task Parallel Library)提供了什么... 它比其他选择略高级,通常是一个经过深思熟虑的库。

对于常规工作任务,您可能想考虑使用TimerSystem.Threading.TimerSystem.Timers.Timer)或甚至是Quartz.NET


你认为这个答案仍然有效吗? - Cer
1
@Cer:它应该仍然可以工作,当然。另一种选择是使用带有取消标记的任务。 - Jon Skeet

10

Thread.Sleep并不是魔鬼,你可以在某些情况下使用它。但对于较短的时间间隔,它并不太可靠。

使用WaitHandle是个不错的选择,但你需要一个特定的实例。但仅仅使用它是不够的。

总的来说,大多数情况下,像这样的操作更适合使用Timer。


2
不,Thread.Sleep并不是魔鬼。它更像是《Dilbert》中的“不足之光王子”菲尔(http://en.wikipedia.org/wiki/List_of_Dilbert_characters#Phil_the_Prince_of_Insufficient_Light)。但说真的,`Thread.Sleep`的好用场景相对较少,在代码中使用它往往是一个警示信号。大多数情况下,将`Sleep`替换为计时器可以使代码更加简洁和健壮。 - Jim Mischel

7

4
您可以使用一个ManualResetEvent来设置停止时间,然后在其上执行WaitOne操作。

3

我建议您使用等待计时器来触发AutoResetEvent。您的线程应该等待这个WaitHandle对象。以下是一个展示这种方法的小型控制台应用程序:

class Program {
    const int TimerPeriod = 5;
    static System.Threading.Timer timer;
    static AutoResetEvent ev;
    static void Main(string[] args) 
    {
        ThreadStart start = new ThreadStart(SomeThreadMethod);
        Thread thr = new Thread(start);
        thr.Name = "background";
        thr.IsBackground = true;
        ev = new AutoResetEvent(false);
        timer = new System.Threading.Timer(
            Timer_TimerCallback, ev, TimeSpan.FromSeconds(TimerPeriod), TimeSpan.Zero);
        thr.Start();
        Console.WriteLine(string.Format("Timer started at {0}", DateTime.Now));
        Console.ReadLine();
    }

    static void Timer_TimerCallback(object state) {
        AutoResetEvent ev =  state as AutoResetEvent;
        Console.WriteLine(string.Format
             ("Timer's callback method is executed at {0}, Thread: ", 
             new object[] { DateTime.Now, Thread.CurrentThread.Name}));
        ev.Set();
    }

    static void SomeThreadMethod() {
        WaitHandle.WaitAll(new WaitHandle[] { ev });
        Console.WriteLine(string.Format("Thread is running at {0}", DateTime.Now));
    }
}

2

正如其他回答者所说,计时器可能适合您。

如果您确实想要自己的线程,在这里我不会使用Thread.Sleep,因为如果您需要关闭应用程序,没有好的方法告诉它退出睡眠。我以前使用过类似于这样的东西。

class IntervalWorker
{
    Thread workerThread;
    ManualResetEventSlim exitHandle = new ManualResetEventSlim();

    public IntervalWorker()
    {
        this.workerThread = new Thread(this.WorkerThread);
        this.workerThread.Priority = ThreadPriority.Lowest;
        this.workerThread.IsBackground = true;
    }

    public void Start()
    {
        this.workerThread.Start();
    }

    public void Stop()
    {
        this.exitHandle.Set();
        this.workerThread.Join();
    }

    private void WorkerThread()
    {
        int waitTimeMillis = 10000; // first work 10 seconds after startup.

        while (!exitHandle.Wait(waitTimeMillis))
        {
            DoWork();

            waitTimeMillis = 300000; // subsequent work at five minute intervals.
        }
    }
}

2
如果你的线程只是在做类似以下的事情:
while (!stop_working)
{
    DoWork();
    Thread.Sleep(FiveMinutes);
}

那我建议根本不要使用线程。首先,没有特别好的理由去承担一个专用线程的系统开销,而这个线程大部分时间都在睡眠。其次,如果你在线程停止睡眠30秒后设置stop_working标志,你将不得不等待四分之一小时,直到线程唤醒并终止。

我建议像其他人一样:使用计时器:

System.Threading.Timer WorkTimer;

// Somewhere in your initialization:

WorkTimer = new System.Threading.Timer((state) =>
    {
        DoWork();
    }, null, TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0));

关机:

 WorkTimer.Dispose();

1

我曾在WinForms应用程序中使用过计时器,也曾在服务器上通过ScheduledTask定期启动控制台应用程序。当我想要弹出通知并显示运行结果时,我会使用带有计时器的WinForms(我将它们设置为最小化到系统托盘)。而当我只想在服务器出现问题时得到通知时,我会将它们作为控制台应用程序放在服务器上,并将其作为Scheduled Tasks运行。这种方法似乎非常有效。

我认为我曾尝试在现在使用计时器的应用程序中使用Sleep,但不喜欢结果。首先,使用计时器可以很容易地调用最小化的应用程序,以便在前端设置或更改设置。如果应用程序处于睡眠状态,您在其沉睡时难以重新获得访问权限。


0
你可以使用 System.Timers.Timer 并在其 Elapse 处理程序中执行工作。

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