C# 带有自动恢复事件的Windows服务

C# 带有自动恢复事件的Windows服务,c#,multithreading,windows-services,C#,Multithreading,Windows Services,我目前正在构建一个Windows服务,它需要处理数据库表中的消息队列。这个队列的长度可能不同,对数据库中的所有行执行可能需要5秒到55秒的时间(我目前使用的是500000条记录的测试数据集) Windows服务被配置为在30秒的计时器上运行,因此我尝试确保当计时器委托运行时,它无法再次运行,直到对该方法的上一个请求成功完成 我的Windows服务OnStart方法中有以下代码: AutoResetEvent autoEvent = new AutoResetEvent(false);

我目前正在构建一个Windows服务,它需要处理数据库表中的消息队列。这个队列的长度可能不同,对数据库中的所有行执行可能需要5秒到55秒的时间(我目前使用的是500000条记录的测试数据集)

Windows服务被配置为在30秒的计时器上运行,因此我尝试确保当计时器委托运行时,它无法再次运行,直到对该方法的上一个请求成功完成

我的Windows服务OnStart方法中有以下代码:

     AutoResetEvent autoEvent = new AutoResetEvent(false);
     TimerCallback timerDelegate = new TimerCallback(MessageQueue.ProcessQueue);

     Timer stateTimer = new Timer(timerDelegate, autoEvent, 1000, Settings.Default.TimerInterval); // TimerInterval is 30000

     autoEvent.WaitOne();
以及MessageQueue.ProcessMessage中的以下代码:

      Trace.Write("Starting ProcessQueue");
      SmtpClient smtp = new SmtpClient("winprev-01");

      AutoResetEvent autoEvent = (AutoResetEvent)stateObject;

      foreach (MessageQueue message in AllUnprocessed)
      {
          switch (message.MessageType)
          {
              case MessageType.PlainText:
              case MessageType.HTML:
                  SendEmail(smtp, message);

                  break;

              case MessageType.SMS:
                  SendSms(message);

                  break;

              default:
                  break;
          }
      }

      autoEvent.Set();
      Trace.Write("Ending ProcessQueue");
我正在使用DebugView分析服务运行时跟踪语句的视图,我可以看到“Starting ProcessQueue”的多个实例每30秒发生一次,这正是我试图避免发生的情况

总之:我想调用ProcessQueue并确保它不会再次执行,除非它完成了工作(这使我能够防止队列中的相同消息被多次处理)

我确信我在这里遗漏了一些非常明显的东西,因此,任何帮助都将不胜感激:)

Dave

启动方法

AutoResetEvent autoEvent = new AutoResetEvent(true);
    while (true)
    {
        autoEvent.WaitOne();
        Thread t = new Thread(MessageQueue.ProcessMessage);             
        t.Start(autoEvent);
    }

您的
ProcessMessage
从不检查是否发出了
resetEvent
信号-它只是在运行

我在这里张贴如何解决这个问题。然而,这不是做你想做的事情的理想方法。请看我答案的底部

您对
autoEvent.WaitOne()
的调用位于错误的位置;它应该位于
ProcessMessage
方法的开头

AutoResetEvent autoEvent = (AutoResetEvent)stateObject;
autoEvent.WaitOne();
Trace.Write("Starting ProcessQueue");
SmtpClient smtp = new SmtpClient("winprev-01");
foreach (MessageQueue message in AllUnprocessed){
您还应该使用接受超时值(int或timespan)的重载,如果方法返回
true
,则返回
bool
,这意味着它已发出信号,因此您可以继续。如果它超时(因为另一个迭代仍在运行),您应该返回,而不是再次尝试运行代码

如果您不使用这样的重载,那么您所做的将无异于将ProcessMessage方法的代码包装在关键部分(例如,在全局变量上的
lock()
),其他线程将被阻塞,然后不必要地运行

AutoResetEvent autoEvent = (AutoResetEvent)stateObject;
//wait just one ms to see if it gets signaled; returns false if not
if(autoEvent.WaitOne(1)){  
    Trace.Write("Starting ProcessQueue");
    SmtpClient smtp = new SmtpClient("winprev-01");
    foreach (MessageQueue message in AllUnprocessed){
请注意,实际上,
*ResetEvent
在这里并不理想。实际上,您只需要检查实例是否已经在运行,如果已经在运行,则中止<代码>重置事件并不是为此而设计的。。。但我还是想解决使用ResetEvent的问题

可能更好的方法是在调用回调时简单地关闭计时器,然后在完成后重新启动计时器。这样,当代码仍在运行时,就不可能重新输入代码


不过,您绝对需要将回调方法中的所有代码包装在一个
try
/
finally
中,以便始终在之后重新启动计时器。

您需要的是一个同步计时器对象。在Win32中,这被称为可等待计时器(不幸的是,除非我弄错了,否则需要一些P/invoke)

以下是您将要做的:

  • 创建可等待计时器(确保其自动重置)
  • 将可等待计时器设置为30秒
  • 循环:
  • 具有无限超时的WaitForSingleObject(可等待计时器)
  • 进程队列

如果处理时间超过30秒,则计时器将保持设置状态,直到您对其调用WaitForSingleObject。此外,例如,如果处理需要20秒,计时器将在10秒后发出信号。

为什么不让您的学员禁用计时器,然后在完成工作后重新启用它(或者继续工作,如果计时器将立即过期)。如果计时器启动和代理唤醒之间的延迟小于30秒,则这应该是无懈可击的

while (true)
{
  Trace.Write("Starting ProcessQueue")
  stateTimer.Enabled = false;
  DateTime start = DateTime.Now;

  // do the work

  // check if timer should be restarted, and for how long
  TimeSpan workTime = DateTime.Now - start;
  double seconds = workTime.TotalSeconds;
  if (seconds > 30)
  {
    // do the work again
    continue;
  }
  else
  {
     // Restart timer to pop at the appropriate time from now
     stateTimer.Interval = 30 - seconds;
     stateTimer.Enabled = true;
     break;
  }
}

您可以通过使用System.Threading.Timer轻松解决此问题。通过将其周期设置为零,使其成为一次性计时器。重新启动回调中的计时器。现在不可能重复执行回调

由于您频繁地运行它,因此另一种方法是使用线程。在OnStop()方法中,需要一个AutoResetEvent来通知线程停止。它的WaitOne()方法在使用接受毫秒计时参数的重载时为您提供一个空闲计时器


顺便说一句:注意OnStart()中的autoEvent.WaitOne()调用很麻烦。如果发送第一封电子邮件需要很长时间,服务控制器可能会超时。忽略它,您启动了计时器==服务启动。

我认为您使这比需要的困难得多。为什么不创建一个单独的线程,围绕一个无限循环旋转,调用
MessageQueue.ProcessQueue
,然后等待一定时间再调用它呢。如果这一切都发生在一个线程上,那么任何事情都不可能并行发生

public class YourService : ServiceBase
{
  private ManualResetEvent m_Stop = new ManualResetEvent(false);

  protected override void OnStart(string[] args)
  {
    new Thread(Run).Start();
  }

  protected override void OnStop()
  {
    m_Stop.Set();
  }

  private void Run()
  {
    while (!m_Stop.WaitOne(TimeSpan.FromSeconds(30))
    {
      MessageQueue.ProcessMessage();
    }
  }
}

为什么不在MessageQueue.ProcessMessage方法的开头和结尾设置一个锁/触发器,然后在再次调用ProcessQueue之前检查它?@Bolu:这不是一个好主意。他会对代码进行迭代,基本上是排队、阻塞、等待第一个完成——然后他们可能会依次运行,然后什么也不做。你真正想要的是一个同步计时器对象——在Win32中,它被称为可等待计时器。@Andrew,当我说lock/trigger时,我指的是一个简单的布尔值。。。。启动ProcessQueue时将其设置为false,完成时将其设置为true,其他迭代只需检查它并中断(如果为false)实际上,如果他要在此处使用计时器,他应该做的是只触发一次,并仅在工作完成时重置计时器。。。根本不使用ResetEvent。这将不起作用;第二次和以后的迭代仍将运行,而不检查resetEvent是否发出了信号。这涉及的内容要多得多