在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");
}
}
答案 0 :(得分:1)
IIS可以/将在工作进程(w3wp)下托管多个AppDomain。这些AppDomain不能/不应该/不应该真正与每个人交谈。 IIS负责管理它们。
我怀疑发生的事情是您加载了多个AppDomain。
那说...只是为了100%确定...计时器是在global.asax的Application_Start下启动的,对吗?每个AppDomain将执行一次(不是每个HttpApplication,顾名思义)。
您可以使用ApplicationManager's GetRunningApplications()
和获取GetAppDomain(string id)
方法检查流程运行的应用域数量。
从理论上讲,你也可以在那里进行一些域间通信,以确保你的流程只开始一次......但我强烈反对它。通常,不建议依赖Web应用程序的调度(因为您的代码不知道IIS如何管理您的应用程序生命周期)。
安排的首选/推荐方法是通过Windows服务。