在 Windows 服务中使用的最佳计时器

我需要创建一些窗口服务,将执行每 N 期间的时间。
问题是:
我应该使用哪个定时器控制: System.Timers.Timer还是 System.Threading.Timer? 它会影响什么吗?

我这样问是因为我听到很多证据表明 System.Timers.Timer在窗口服务中的工作不正确。
谢谢你。

203213 次浏览

Either one should work OK. In fact, System.Threading.Timer uses System.Timers.Timer internally.

Having said that, it's easy to misuse System.Timers.Timer. If you don't store the Timer object in a variable somewhere, then it is liable to be garbage collected. If that happens, your timer will no longer fire. Call the Dispose method to stop the timer, or use the System.Threading.Timer class, which is a slightly nicer wrapper.

What problems have you seen so far?

Both System.Timers.Timer and System.Threading.Timer will work for services.

The timers you want to avoid are System.Web.UI.Timer and System.Windows.Forms.Timer, which are respectively for ASP applications and WinForms. Using those will cause the service to load an additional assembly which is not really needed for the type of application you are building.

Use System.Timers.Timer like the following example (also, make sure that you use a class level variable to prevent garbage collection, as stated in Tim Robinson's answer):

using System;
using System.Timers;


public class Timer1
{
private static System.Timers.Timer aTimer;


public static void Main()
{
// Normally, the timer is declared at the class level,
// so that it stays in scope as long as it is needed.
// If the timer is declared in a long-running method,
// KeepAlive must be used to prevent the JIT compiler
// from allowing aggressive garbage collection to occur
// before the method ends. (See end of method.)
//System.Timers.Timer aTimer;


// Create a timer with a ten second interval.
aTimer = new System.Timers.Timer(10000);


// Hook up the Elapsed event for the timer.
aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);


// Set the Interval to 2 seconds (2000 milliseconds).
aTimer.Interval = 2000;
aTimer.Enabled = true;


Console.WriteLine("Press the Enter key to exit the program.");
Console.ReadLine();


// If the timer is declared in a long-running method, use
// KeepAlive to prevent garbage collection from occurring
// before the method ends.
//GC.KeepAlive(aTimer);
}


// Specify what you want to happen when the Elapsed event is
// raised.
private static void OnTimedEvent(object source, ElapsedEventArgs e)
{
Console.WriteLine("The Elapsed event was raised at {0}", e.SignalTime);
}
}


/* This code example produces output similar to the following:


Press the Enter key to exit the program.
The Elapsed event was raised at 5/20/2007 8:42:27 PM
The Elapsed event was raised at 5/20/2007 8:42:29 PM
The Elapsed event was raised at 5/20/2007 8:42:31 PM
...
*/

If you choose System.Threading.Timer, you can use as follows:

using System;
using System.Threading;


class TimerExample
{
static void Main()
{
AutoResetEvent autoEvent     = new AutoResetEvent(false);
StatusChecker  statusChecker = new StatusChecker(10);


// Create the delegate that invokes methods for the timer.
TimerCallback timerDelegate =
new TimerCallback(statusChecker.CheckStatus);


// Create a timer that signals the delegate to invoke
// CheckStatus after one second, and every 1/4 second
// thereafter.
Console.WriteLine("{0} Creating timer.\n",
DateTime.Now.ToString("h:mm:ss.fff"));
Timer stateTimer =
new Timer(timerDelegate, autoEvent, 1000, 250);


// When autoEvent signals, change the period to every
// 1/2 second.
autoEvent.WaitOne(5000, false);
stateTimer.Change(0, 500);
Console.WriteLine("\nChanging period.\n");


// When autoEvent signals the second time, dispose of
// the timer.
autoEvent.WaitOne(5000, false);
stateTimer.Dispose();
Console.WriteLine("\nDestroying timer.");
}
}


class StatusChecker
{
int invokeCount, maxCount;


public StatusChecker(int count)
{
invokeCount  = 0;
maxCount = count;
}


// This method is called by the timer delegate.
public void CheckStatus(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
Console.WriteLine("{0} Checking status {1,2}.",
DateTime.Now.ToString("h:mm:ss.fff"),
(++invokeCount).ToString());


if(invokeCount == maxCount)
{
// Reset the counter and signal Main.
invokeCount  = 0;
autoEvent.Set();
}
}
}

Both examples comes from the MSDN pages.

Don't use a service for this. Create a normal application and create a scheduled task to run it.

This is the commonly held best practice. Jon Galloway agrees with me. Or maybe its the other way around. Either way, the fact is that it is not best practices to create a windows service to perform an intermittent task run off a timer.

"If you're writing a Windows Service that runs a timer, you should re-evaluate your solution."

–Jon Galloway, ASP.NET MVC community program manager, author, part time superhero

I agree with previous comment that might be best to consider a different approach. My suggest would be write a console application and use the windows scheduler:

This will:

  • Reduce plumbing code that replicates scheduler behaviour
  • Provide greater flexibility in terms of scheduling behaviour (e.g. only run on weekends) with all scheduling logic abstracted from application code
  • Utilise the command line arguments for parameters without having to setup configuration values in config files etc
  • Far easier to debug/test during development
  • Allow a support user to execute by invoking the console application directly (e.g. useful during support situations)

As already stated both System.Threading.Timer and System.Timers.Timer will work. The big difference between the two is that System.Threading.Timer is a wrapper arround the other one.

System.Threading.Timer will have more exception handling while System.Timers.Timer will swallow all the exceptions.

This gave me big problems in the past so I would always use 'System.Threading.Timer' and still handle your exceptions very well.

I know this thread is a little old but it came in handy for a specific scenario I had and I thought it worth while to note that there is another reason why System.Threading.Timer might be a good approach. When you have to periodically execute a Job that might take a long time and you want to ensure that the entire waiting period is used between jobs or if you don't want the job to run again before the previous job has finished in the case where the job takes longer than the timer period. You could use the following:

using System;
using System.ServiceProcess;
using System.Threading;


public partial class TimerExampleService : ServiceBase
{
private AutoResetEvent AutoEventInstance { get; set; }
private StatusChecker StatusCheckerInstance { get; set; }
private Timer StateTimer { get; set; }
public int TimerInterval { get; set; }


public CaseIndexingService()
{
InitializeComponent();
TimerInterval = 300000;
}


protected override void OnStart(string[] args)
{
AutoEventInstance = new AutoResetEvent(false);
StatusCheckerInstance = new StatusChecker();


// Create the delegate that invokes methods for the timer.
TimerCallback timerDelegate =
new TimerCallback(StatusCheckerInstance.CheckStatus);


// Create a timer that signals the delegate to invoke
// 1.CheckStatus immediately,
// 2.Wait until the job is finished,
// 3.then wait 5 minutes before executing again.
// 4.Repeat from point 2.
Console.WriteLine("{0} Creating timer.\n",
DateTime.Now.ToString("h:mm:ss.fff"));
//Start Immediately but don't run again.
StateTimer = new Timer(timerDelegate, AutoEventInstance, 0, Timeout.Infinite);
while (StateTimer != null)
{
//Wait until the job is done
AutoEventInstance.WaitOne();
//Wait for 5 minutes before starting the job again.
StateTimer.Change(TimerInterval, Timeout.Infinite);
}
//If the Job somehow takes longer than 5 minutes to complete then it wont matter because we will always wait another 5 minutes before running again.
}


protected override void OnStop()
{
StateTimer.Dispose();
}
}


class StatusChecker
{


public StatusChecker()
{
}


// This method is called by the timer delegate.
public void CheckStatus(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
Console.WriteLine("{0} Start Checking status.",
DateTime.Now.ToString("h:mm:ss.fff"));
//This job takes time to run. For example purposes, I put a delay in here.
int milliseconds = 5000;
Thread.Sleep(milliseconds);
//Job is now done running and the timer can now be reset to wait for the next interval
Console.WriteLine("{0} Done Checking status.",
DateTime.Now.ToString("h:mm:ss.fff"));
autoEvent.Set();
}
}