Using a timer in an ASP.NET application is not as simple as it seems.  There are three problems that commonly occur:

  1. ASP.NET applications shutdown for a number of reasons, and if the timer is not disposed before the AppDomain is unloaded, the timer may fire and crash the process with an unhandled AppDomainUnloadedException.
  2. The timer executes its callback on CLR Threadpool threads.  If the interval is small and the server is under a heavy load, the CLR Threadpool can queue up many work items.  This could result in multiple timer callbacks firing at or about the same time.  Or, if the timer callback itself takes a long time to execute, this could result in multiple concurrent callers.  Developers often incorrectly assume that only one thread will call the timer concurrently.
  3. If your timer callback invokes native code, via a P/Invoke or COM Interop call, and the AppDomain is unloaded while that code is being called, it may result in an unhandled AppDomainUnloadedException and crash the process.

These problems are easily avoided.  There are a number of ways to be notified before the AppDomain is unloaded.  Within ASP.NET applications, you can listen to the Application_End event.  Another way would be to listen to the AppDomain.DomainUnload event.  Regardless of how you are notified, you’ll want to use code similar to the following to protect yourself against the aforementioned problems:

 

// 1 if timer callback is executing; otherwise 0
int _inTimerCallback = 0;

Timer _timer = new Timer(new TimerCallback(this.TimerCallback), 
                         null, 
                         1000, 
                         1000);
//
// if the timer fires frequently or the callback runs for a long period, 
// you may want to prevent two threads from calling it concurrently
//
void TimerCallback(object state) {
    // if the callback is already being executed, just return
    if (Interlocked.Exchange(ref _inTimerCallback, 1) != 0) {
        return;
    }
    try {
        // do work (potentially long running work that 
        // may call into native code)
    }
    finally {
        Interlocked.Exchange(ref _inTimerCallback, 0);
    }
}

//
// Before the AppDomain is shutdown, the timer must be disposed.  Otherwise,
// the underlying native timer may crash the process if it fires and attempts
// to call into the unloaded AppDomain.  In a multi-threaded environment,
// you may need to use synchronization to ensure the timer is disposed at 
// most once.
//
internal void DisposeTimer() {
    Timer timer = _timer;
    if (timer != null
        && Interlocked.CompareExchange(ref _timer, null, timer) == timer) {
            timer.Dispose();
        }
    }

    // if you don’t want the timer callback to be aborted during an 
    // AppDomain unload, or if it calls into native code, then loop until 
    // the callback has completed
    while (_inTimerCallback != 0) {
        Sleep(100);
    }
}