如何让计时器在上一个线程仍然忙碌的情况下跳过 tick。

15

我创建了一个Windows服务,该服务每60秒检查数据库中的某个表是否有新行。对于每个新添加的行,我需要在服务器上进行一些繁重的处理,这可能需要超过60秒的时间。

我在服务中创建了一个计时器对象,它每60秒会触发并调用所需的方法。
由于我不希望在处理找到的新行时计时器继续触发,所以我将方法包装在lock { }块中,以便其他线程无法访问该方法。

大致看起来是这样的:

Timer serviceTimer = new Timer();
serviceTimer.Interval = 60;
serviceTimer.Elapsed += new ElapsedEventHandler(serviceTimer_Elapsed);
serviceTimer.Start();

void serviceTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    lock (this)
    {
        // do some heavy processing...
    }
}

现在,我在思考 -
如果我的计时器滴答作响,发现数据库上有很多新行,并且现在处理需要超过60秒,那么下一个滴答将不会进行任何处理,直到上一个完成。这就是我想要的效果。

但现在,当第一次处理完成后,serviceTimer_Elapsed方法会立即触发吗,还是它会等待计时器再次滴答?

我想要的是 - 如果处理需要超过60秒,则计时器将注意到线程被锁定,并等待另外60秒才能再次检查,以便我永远不会陷入等待前一个处理完成的线程队列中。

我如何实现这个结果?
在这方面有什么最佳实践吗?

谢谢!

8个回答

21
您可以尝试在处理过程中禁用计时器,类似这样的操作:
// Just in case someone wants to inherit your class and lock it as well ...
private static object _padlock = new object();
try
{
  serviceTimer.Stop(); 

  lock (_padlock)
    { 
        // do some heavy processing... 
    } 
}
finally
{
  serviceTimer.Start(); 
}

编辑:原帖作者没有明确指定重入是由计时器还是多线程服务引起的。本人假设是后者,但如果是前者,则在停止计时器(自动重置或手动)时无需进行锁定。


11
不要使用lock (this) - https://dev59.com/kXVC5IYBdhLWcg3wjyDu - Tim Robinson
3
将autoreset设置为false会更容易,这样我们就不需要锁定任何东西。 - Oleg Vazhnev

20

在这种情况下,您不需要锁定。在启动计时器之前设置timer.AutoReset=false。 在处理完成后,在处理程序中重新启动计时器。这将确保计时器在每个任务之后60秒触发。


8
在其他答案的基础上,还有一种类似的变体,它允许计时器继续运行并在可以获取锁时执行工作,而不是停止计时器。
将以下代码放入已过去的事件处理程序中:
if (Monitor.TryEnter(locker)
{
    try
    {
        // Do your work here.
    }
    finally
    {
        Monitor.Exit(locker);
    }
}

这也是我使用的方法。经过的事件处理程序只是掉落,计时器可以被单独留下。 - Ed Power

7

快速检查服务是否正在运行。如果它正在运行,它将跳过此事件并等待下一个事件触发。

Timer serviceTimer = new Timer();
serviceTimer.Interval = 60;
serviceTimer.Elapsed += new ElapsedEventHandler(serviceTimer_Elapsed);
serviceTimer.Start();
bool isRunning = false;
void serviceTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    lock (this)
    {
        if(isRunning)
            return;
        isRunning = true;
    }
    try
    {
    // do some heavy processing...
    }
    finally
    {
        isRunning = false;
    }
}

1
我更喜欢nonnb的解决方案,但我会保留我的示例,以便在您无法停止事件触发时提供一个示例。 - Scott Chamberlain
此外,在某些情况下,允许事件继续触发会非常有用。您可以记录它或执行一些与主进程外部相关的处理。 - Tawab Wakil

4

2
其他选项可能是使用BackGroundWorker类或ThreadPool.QueueUserWorkItem。
Background worker可以轻松地让您选择检查当前处理是否仍在进行并逐个处理项目。线程池将使您能够继续每个tick(如果必要)将项目排队到后台线程。
根据您的描述,我假设您正在检查数据库中的队列中的项目。在这种情况下,我会使用ThreadPool将工作推送到后台,并且不会减慢/停止您的检查机制。
对于服务,我真的建议您考虑使用ThreadPool方法。这样,您可以使用计时器每60秒检查新项目,然后将它们排队,让.Net确定为每个项目分配多少,并继续将项目推入队列中。
例如:如果您只使用计时器,并且有5个新行需要总共65秒的处理时间。使用ThreadPool方法,这将在65秒内完成,有5个后台工作项。使用计时器方法,这将花费4+分钟(您将等待每行之间的1分钟),而且可能会导致其他正在排队的工作积压。
以下是如何执行此操作的示例:
Timer serviceTimer = new Timer();
    void startTimer()
    {
        serviceTimer.Interval = 60;
        serviceTimer.Elapsed += new ElapsedEventHandler(serviceTimer_Elapsed);
        serviceTimer.AutoReset = false;
        serviceTimer.Start();
    }
    void serviceTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        try
        {
            // Get your rows of queued work requests

            // Now Push Each Row to Background Thread Processing
            foreach (Row aRow in RowsOfRequests)
            {
                ThreadPool.QueueUserWorkItem(
                    new WaitCallback(longWorkingCode), 
                    aRow);
            }
        }
        finally
        {
            // Wait Another 60 Seconds and check again
            serviceTimer.Stop();
        }
    }

    void longWorkingCode(object workObject)
    {
        Row workRow = workObject as Row;
        if (workRow == null)
            return;

        // Do your Long work here on workRow
    }

2

使用响应式扩展(Reactive Extensions)可以很好地解决这个问题。以下是代码,您可以在这里阅读更详细的解释:http://www.zerobugbuild.com/?p=259

public static IDisposable ScheduleRecurringAction(
    this IScheduler scheduler,
    TimeSpan interval,
    Action action)
{
    return scheduler.Schedule(
        interval, scheduleNext =>
    {
        action();
        scheduleNext(interval);
    });
}

你可以像这样使用它:

TimeSpan interval = TimeSpan.FromSeconds(5);
Action work = () => Console.WriteLine("Doing some work...");

var schedule = Scheduler.Default.ScheduleRecurringAction(interval, work);          

Console.WriteLine("Press return to stop.");
Console.ReadLine();
schedule.Dispose();

2
这是一个相当不错的解决方案,并展示了 RX 调度程序的有趣用途,可能并不立即显而易见。 - jamesmus
这个解决方案对我很有效。让我对调度程序有了新的认识。 - jumpercake

0

另一种可能性是这样的:

void serviceTimer_Elapsed(object sender, ElapsedEventArgs e)
{   
    if (System.Threading.Monitor.IsLocked(yourLockingObject))
       return;
    else
       lock (yourLockingObject)
       // your logic  
           ;
}

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