【问题标题】:Windows Service with AutoResetEvent带有 AutoResetEvent 的 Windows 服务
【发布时间】:2010-11-17 11:01:24
【问题描述】:

我目前正在构建一个 Windows 服务,该服务需要处理位于数据库表中的消息队列。该队列的长度可能不同,并且可能需要 5 秒到 55 秒来针对数据库中的所有行执行(我目前正在使用包含 500,000 条记录的测试数据集)

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

p>

我的 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 在服务运行时分析 Trace 语句的视图,我可以看到每 30 秒发生一次的多个“Starting ProcessQueue”实例,这是我试图避免发生的情况

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

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

戴夫

【问题讨论】:

  • 为什么不在 MessageQueue.ProcessMessage 方法的开头和结尾设置一个锁/触发器,然后在再次调用 ProcessQueue 之前检查它?
  • @Bolu:这不是一个好主意。他的代码迭代基本上是排队、阻塞、等待第一个完成——然后大概他们每个人都会依次运行,然后无事可做。
  • 你真正想要的是一个同步定时器对象——在 Win32 中,它被称为可等待定时器。
  • @Andrew,当我说锁定/触发器时,我的意思是一个简单的布尔值......在启动 ProcessQueue 时将其设置为 false,并在完成时将其设置为 true,其他迭代只需检查它并爆发(如果是假的)
  • 其实,如果他要在这里使用定时器,他应该只触发一次,并且只有在工作完成时才重置定时器......根本不使用ResetEvent。

标签: c# multithreading windows-services


【解决方案1】:

您为什么不让您的委托禁用计时器,然后在它完成工作后重新启用它(或继续工作,如果计时器将立即到期)。如果计时器触发和您的委托人醒来之间的延迟小于 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;
  }
}

【讨论】:

  • +1 ;我通过在最后发布来混淆我的答案:p
  • 接受为答案。正如史蒂夫和安德鲁都提到的那样,这是解决问题的正确方法,而不仅仅是回答我的问题 - 谢谢你们两个:)
【解决方案2】:

您的 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 或时间跨度)并返回bool 的重载,如果该方法返回true,则表示它已发出信号,因此您可以继续。如果超时(因为另一个迭代仍在运行),您应该返回而不尝试再次运行代码。

如果你不使用这样的重载,你所做的与将 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 在这里并不理想。你真的只是想检查一个实例是否已经在运行,如果是,则中止。 ResetEvents 并不是为此而生的……但我还是想解决使用 ResetEvent 的问题。

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

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

【讨论】:

  • +1 用于回答问题,但为我指明了完成工作的正确方法的方向(认为我的解决方案稍微过度设计了)
【解决方案3】:

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

由于您如此频繁地运行此程序,因此另一种方法是使用线程。您需要一个 AutoResetEvent 来指示线程在 OnStop() 方法中停止。当您使用采用 millisecondsTimeout 参数的重载时,它的 WaitOne() 方法会为您提供一个空闲计时器。

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

【讨论】:

    【解决方案4】:

    我认为你让这件事变得比需要的困难得多。为什么不创建一个单独的线程,围绕一个无限循环旋转调用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();
        }
      }
    }
    

    【讨论】:

      【解决方案5】:

      OnStart 方法

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

      【讨论】:

      • 那行不通;第二次及以后的迭代仍将运行,而不检查是否已发出 resetEvent 信号。
      【解决方案6】:

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

      你会这样做:

      • 创建可等待计时器(确保它是自动重置的)。
      • 将可等待计时器设置为 30 秒。
      • 循环:
      • WaitForSingleObject(等待计时器)无限超时。
      • 处理队列。

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

      【讨论】:

      • 这比他需要的要多得多,而且以间接的方式,这就是他已经完成的事情。 .NET ResetEvent 对象调用您引用的 API。
      • 不,他的代码非常不同,它使用定时器对象的回调功能,而不是等待定时器本身。另外,为什么说 Event 类使用可等待的计时器函数?
      猜你喜欢
      • 2011-05-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多