我目前正在构建一个Windows服务,它需要处理一个位于数据库表中的消息队列。此队列的长度可能不同,可能需要5秒到55秒才能对数据库中的所有行执行(我目前正在使用500,000条记录的测试数据集)
Windows服务配置为在30秒计时器上运行,因此我尝试了,但未成功,以确保在计时器委托运行时,在方法的上一个请求成功完成之前无法再次运行
我的Windows Service 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并确保它不会再次执行,除非它已完成其工作(这使我能够防止队列中的相同消息被多次处理
我确信我错过了一些非常明显的东西,所以任何帮助都会非常感激:)
戴夫
答案 0 :(得分: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或timepan)的重载,并返回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
在这里并不理想。您真的只想检查实例是否已在运行,如果是,则中止。 ResetEvent
并不是真的为此做出的......但我想解决使用ResetEvent的问题。
更好的方法是在调用回调时简单地关闭计时器,然后在完成后重新启动计时器。这样,在代码仍在运行时,不可能重新输入该代码。
你绝对需要将回调方法中的所有代码包装在try
/ finally
中,这样你总是重新启动计时器。
答案 1 :(得分:2)
为什么不让你的代理人禁用定时器,然后重新启用它(或者如果定时器会立即过期,继续工作)一旦完成工作。假设定时器触发和你的代表唤醒之间的延迟是< 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;
}
}
答案 2 :(得分:1)
您可以使用System.Threading.Timer轻松解决此问题。通过将期间设置为零,可以使其成为一次性计时器。在回调中重新启动计时器。现在无法重写执行回调。
由于您经常这样运行,因此另一种方法是使用线程。您需要一个AutoResetEvent来指示线程在OnStop()方法中停止。当您使用带有 millisecondsTimeout 参数的重载时,它的WaitOne()方法为您提供一个空闲计时器。
顺便说一句:请注意OnStart()中的autoEvent.WaitOne()调用很麻烦。如果第一封电子邮件需要很长时间才能发送,它可能会超时服务控制器。省略它,你得到了计时器启动==服务开始。
答案 3 :(得分:0)
OnStart方法
AutoResetEvent autoEvent = new AutoResetEvent(true);
while (true)
{
autoEvent.WaitOne();
Thread t = new Thread(MessageQueue.ProcessMessage);
t.Start(autoEvent);
}
答案 4 :(得分:0)
您想要的是同步计时器对象。在Win32中,这被称为等待计时器(不幸的是,除非我弄错了,否则需要一些P / invoke。)
这是你要做的:
如果处理时间超过30秒,定时器将保持设置状态,直到您在其上调用WaitForSingleObject。此外,如果处理时间为20秒,则计时器将在10秒后发出信号。
答案 5 :(得分:0)
我认为你正在努力实现这一目标。为什么不创建一个单独的线程,围绕一个无限循环调用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();
}
}
}