如何防止Global.asax中的双倍计时器?

时间:2011-12-01 18:36:20

标签: c# asp.net timer

说明

在C#ASP.Net Web应用程序中,我们已经实现了一些定时器来定期运行后台任务。其中一个计时器偶尔似乎会“加倍”或更少“三倍”。

计时器设置为每分钟运行一次,似乎运行一段时间。然而,最终,似乎第二个计时器开始并在同一时间间隔内第二次调用定时进程。我甚至看到过一个我们有三个进程在运行的情况。

由于此进程锁定了一些数据库记录并且让第二个(或第三个)进程执行相同操作会导致数据库连接出现死锁或超时错误,因此我们实现了一种机制,一次只允许一个线程执行进程代码的数据库关键部分。当进程运行时间超过一分钟时,此机制会成功阻止由其自己的计时器触发的下一次运行。但如果进程由第二个(或第三个)计时器触发,则线程锁定失败。

在我们的日志中,我输出了进程ID和托管线程ID,这使我可以看到哪个线程正在启​​动,完成或错误输出。奇怪的是,无论哪个计时器实例启动进程,进程ID都是相同的。

var processID = System.Diagnostics.Process.GetCurrentProcess().Id;
var thread = System.Threading.Thread.CurrentThread.ManagedThreadId;

如何阻止多个计时器实例?

我们在负载均衡器后面有一个带有2台服务器的Web场。我一直被告知,web-garden被设置为只允许每个服务器上的一个app-pool实例。 web.config设置指定哪个服务器将运行定时进程。另一台服务器不会加载计时器。

相关守则:

在Global.asax.cs

protected static WebTaskScheduler PersonGroupUpdateScheduler
{
    get;
    private set;
}

protected void StartSchedulers()
{
    using (var logger = new LogManager())
    {
        // ... other timers configured in similar fashion ...

        if (AppSetting.ContinuousPersonGroupUpdates)
        {
            // clear out-of-date person-group-updater lock
            logger.AppData.Remove("PersonGroupUpdater"); // database record to prevent interference with another process outside the web application.

            var currentServer = System.Windows.Forms.SystemInformation.ComputerName;
            if (currentServer.EqualsIngoreCase(AppSetting.ContinuousPersonGroupUpdateServer))
            {
                PersonGroupUpdateScheduler = new WebTaskScheduler() {
                    AutoReset = true,
                    Enabled = true,
                    Interval = AppSetting.ContinuousPersonGroupUpdateInterval.TotalMilliseconds,
                    SynchronizingObject = null,
                };
                PersonGroupUpdateScheduler.Elapsed += new ElapsedEventHandler(DistributePersonGroupProcessing);
                PersonGroupUpdateScheduler.Start();
                HostingEnvironment.RegisterObject(PersonGroupUpdateScheduler);
                logger.Save(Log.Types.Info, "Starting Continuous Person-Group Updating Timer.", "Web");
            }
            else
            {
                logger.Save(Log.Types.Info, string.Format("Person-Group Updating set to run on server {0}.", AppSetting.ContinuousPersonGroupUpdateServer), "Web");
            }
        }
        else
        {
            logger.Save(Log.Types.Info, "Person-Group Updating is turned off.", "Web");
        }
    }
}

private void DistributePersonGroupProcessing(object state, ElapsedEventArgs eventArgs)
{
    // to start with a clean connection, create a new data context (part of default constructor)
    // with each call.
    using (var groupUpdater = new GroupManager())
    {
        groupUpdater.HttpContext = HttpContext.Current;
        groupUpdater.ContinuousGroupUpdate(state, eventArgs);
    }
}

在一个单独的文件中,我们有一个WebTaskScheduler类,它只包装System.Timers.Timer并实现IRegisteredObject接口,这样IIS就会将触发的进程识别为关闭时需要处理的内容。

public class WebTaskScheduler : Timer, IRegisteredObject
{
    private Action _action = null;
    public Action Action
    {
        get
        {
            return _action;
        }
        set
        {
            _action = value;
        }
    }

    private readonly WebTaskHost _webTaskHost = new WebTaskHost();

    public WebTaskScheduler()
    {
    }

    public void Stop(bool immediate)
    {
        this.Stop();
        _action = null;
    }
}

最后,代码的关键部分的锁定机制。

public void ContinuousGroupUpdate(object state, System.Timers.ElapsedEventArgs eventArgs)
{
    var pgUpdateLock = PersonGroupUpdaterLock.Instance;

    try
    {
        if (0 == Interlocked.Exchange(ref pgUpdateLock.LockCounter, 1))
        {
            if (LogManager.AppData["GroupImporter"] == "Running")
            {
                Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
                LogManager.Save(Log.Types.Info, string.Format("Group Import is running, exiting Person-Group Updater.  Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
                return;
            }

            try
            {
                LogManager.Save(Log.Types.Info, string.Format("Continuous Person-Group Update is Starting.  Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
                LogManager.AppData["PersonGroupUpdater"] = "Running";

                // ... prep work is done here ...

                try
                {
                    // ... real work is done here ...

                    LogManager.Save(Log.Types.Info, "Continuous Person-Group Update is Complete", "Person-Group Updater");

                }
                catch (Exception ex)
                {
                    ex.Data["Continuous Person-Group Update Activity"] = "Processing Groups";
                    ex.Data["Current Record when failure occurred"] = currentGroup ?? string.Empty;
                    LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
                }

            }
            catch (Exception ex)
            {
                LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
            }
            finally
            {
                Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
                LogManager.AppData.Remove("PersonGroupUpdater");
            }

        }
        else
        {
            // exit if another thread is already running this method
            LogManager.Save(Log.Types.Info, string.Format("Continuous Person-Group Update is already running, exiting Person-Group Updater.  Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
        }
    }
    catch (Exception ex)
    {
        Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
        LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
    }
}

1 个答案:

答案 0 :(得分:1)

IIS可以/将在工作进程(w3wp)下托管多个AppDomain。这些AppDomain不能/不应该/不应该真正与每个人交谈。 IIS负责管理它们。

我怀疑发生的事情是您加载了多个AppDomain。

那说...只是为了100%确定...计时器是在global.asax的Application_Start下启动的,对吗?每个AppDomain将执行一次(不是每个HttpApplication,顾名思义)。

您可以使用ApplicationManager's GetRunningApplications()和获取GetAppDomain(string id)方法检查流程运行的应用域数量。

从理论上讲,你也可以在那里进行一些域间通信,以确保你的流程只开始一次......但我强烈反对它。通常,不建议依赖Web应用程序的调度(因为您的代码不知道IIS如何管理您的应用程序生命周期)。

安排的首选/推荐方法是通过Windows服务。