我有一个Web应用程序,可以控制哪些Web应用程序从我们的负载均衡器获得流量。 Web应用程序在每台服务器上运行。
它跟踪ASP.NET应用程序状态中对象中每个应用程序的“进入或退出”状态,并且只要状态发生更改,就将对象序列化为磁盘上的文件。 Web应用程序启动时,将从文件中反序列化状态。
虽然网站本身只获得了一些请求,但是它很少访问的文件,我发现在尝试读取或写入文件时,由于某种原因很容易发生冲突。这种机制需要非常可靠,因为我们有一个自动化系统,可以定期对服务器进行滚动部署。
在任何人发表任何评论以质疑上述任何一项的谨慎之前,请允许我简单地说解释其背后的推理会使这篇文章比现在更长,所以我想避免搬山。 / p>
也就是说,我用来控制文件访问的代码如下所示:
internal static Mutex _lock = null;
/// <summary>Executes the specified <see cref="Func{FileStream, Object}" /> delegate on
/// the filesystem copy of the <see cref="ServerState" />.
/// The work done on the file is wrapped in a lock statement to ensure there are no
/// locking collisions caused by attempting to save and load the file simultaneously
/// from separate requests.
/// </summary>
/// <param name="action">The logic to be executed on the
/// <see cref="ServerState" /> file.</param>
/// <returns>An object containing any result data returned by <param name="func" />.
///</returns>
private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
var l = new Logger();
if (ServerState._lock.WaitOne(1500, false))
{
l.LogInformation( "Got lock to read/write file-based server state."
, (Int32)VipEvent.GotStateLock);
var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate
, FileAccess.ReadWrite, FileShare.None);
result = func.Invoke(fileStream);
fileStream.Close();
fileStream.Dispose();
fileStream = null;
ServerState._lock.ReleaseMutex();
l.LogInformation( "Released state file lock."
, (Int32)VipEvent.ReleasedStateLock);
return true;
}
else
{
l.LogWarning( "Could not get a lock to access the file-based server state."
, (Int32)VipEvent.CouldNotGetStateLock);
result = null;
return false;
}
}
此通常有效,但偶尔我无法访问互斥锁(我在日志中看到“无法获取锁定”事件)。我无法在本地重现 - 它只发生在我的生产服务器(Win Server 2k3 / IIS 6)上。如果我删除超时,应用程序将无限期挂起(竞争条件??),包括后续请求。
当我收到错误时,查看事件日志会告诉我,在记录错误之前,上一个请求已实现并释放了互斥锁。
互斥锁在Application_Start事件中实例化。在声明中静态实例化时,我得到相同的结果。
借口,借口:穿线/锁定不是我的强项,因为我一般不用担心它。
关于为什么它会随机发出信号的任何建议?
更新
我添加了正确的错误处理(多么令人尴尬!),但我仍然得到相同的错误 - 而且对于记录,未处理的异常从来都不是问题。
只有一个进程可以访问该文件 - 我没有为此应用程序的Web池使用Web园,也没有其他应用程序使用该文件。我能想到的唯一例外是当应用程序池回收时,旧的WP在创建新的WP时仍然打开 - 但是我可以通过观察任务管理器告知在只有一个工作进程时问题发生。
@mmr:如何使用Monitor与使用Mutex有什么不同?根据MSDN文档,看起来它实际上是在做同样的事情 - 如果我无法通过我的Mutex获得锁定,那么 会因为返回false而优雅地失败。
另一件需要注意的事情是:我遇到的问题似乎是完全随机的 - 如果它在一个请求中失败,它可能在下一个工作正常。似乎没有一种模式(至少没有其他模式)。
更新2:
此锁定不用于任何其他呼叫。在InvokeOnFile方法之外引用_lock的唯一时间是实例化它。
调用的Func要么从文件读取并反序列化为对象,要么序列化对象并将其写入文件。这两个操作都不是在单独的线程上完成的。
ServerState.PATH是一个静态只读字段,我不希望这会导致任何并发问题。
我还想重复我之前的观点,即我无法在本地重现(在卡西尼号中)。
经验教训:
答案 0 :(得分:15)
如果您需要跨进程同步,则应该只使用互斥锁。
虽然可以使用互斥锁 进程内线程同步, 使用Monitor通常是首选, 因为显示器是设计的 专门针对.NET Framework 因此,更好地利用 资源。相比之下,互斥体 class是Win32的包装器 构造。虽然它更强大 互联网要求,而不是监视器 互操作过渡更多 计算上比那些贵 Monitor类要求。
如果您需要支持进程间锁定,则需要Global mutex。
使用的模式非常脆弱,没有异常处理,并且您无法确保释放Mutex。这是非常冒险的代码,很可能是你没有超时的原因。
此外,如果您的文件操作时间超过1.5秒,那么并发互斥锁将无法抓取它。我建议正确锁定并避免超时。
我认为最好重新编写这个以使用锁。此外,看起来你正在呼唤另一种方法,如果这需要永远,锁将永远保持。这很危险。
这既短又安全:
// if you want timeout support use
// try{var success=Monitor.TryEnter(m_syncObj, 2000);}
// finally{Monitor.Exit(m_syncObj)}
lock(m_syncObj)
{
l.LogInformation( "Got lock to read/write file-based server state."
, (Int32)VipEvent.GotStateLock);
using (var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate
, FileAccess.ReadWrite, FileShare.None))
{
// the line below is risky, what will happen if the call to invoke
// never returns?
result = func.Invoke(fileStream);
}
}
l.LogInformation("Released state file lock.", (Int32)VipEvent.ReleasedStateLock);
return true;
// note exceptions may leak out of this method. either handle them here.
// or in the calling method.
// For example the file access may fail of func.Invoke may fail
答案 1 :(得分:2)
如果某些文件操作失败,则不会释放锁定。很可能就是这种情况。将文件操作放在try / catch块中,并释放finally块中的锁。
无论如何,如果您在Global.asax Application_Start方法中读取该文件,这将确保没有其他人在使用它(您说在应用程序启动时读取该文件,对吧?)。为了避免应用程序池恢复等的冲突,您可以尝试读取该文件(假设写入操作采用独占锁定),然后等待1秒钟,如果抛出异常则重试。
现在,您遇到了同步写入的问题。无论什么方法决定更改文件,如果另一个正在使用简单的锁定语句,请注意不要调用写操作。
答案 2 :(得分:0)
我在这里看到了几个潜在的问题。
编辑更新2:如果函数是一个简单的序列化/反序列化组合,我将两个函数分成两个不同的函数,一个用于'序列化'函数,另一个用于'反序列化'函数。他们真的是两个不同的任务。然后,您可以拥有不同的特定于锁的任务。 Invoke很漂亮,但是我自己在'工作'上'漂亮'时遇到了很多麻烦。
1)你的LogInformation函数是否锁定?因为您首先在互斥锁中调用它,然后在释放互斥锁后再调用它。因此,如果有一个锁写入日志文件/结构,那么你可以在那里结束你的竞争条件。为避免这种情况,请将日志放入锁中。
2)使用Monitor类检查,我知道它在C#中工作,我假设在ASP.NET中工作。为此,您只需尝试获取锁定,否则优先失败。使用它的一种方法是继续尝试获取锁定。 (编辑原因:请参阅here;基本上,互斥程序跨进程,Monitor只在一个进程中,但是专为.NET设计,因此是首选。文档没有给出其他真正的解释。)
3)如果文件流打开失败会发生什么,因为其他人有锁?这会引发异常,这可能会导致此代码表现不佳(即,锁定仍由具有异常的线程保持,另一个线程可以获取它)。
4)func本身怎么样?这会启动另一个线程,还是完全在一个线程内?那么访问ServerState.PATH呢?
5)还有哪些其他功能可以访问ServerState._lock?我更喜欢每个需要锁的函数都有自己的锁,以避免竞争/死锁条件。如果你有很多线程,并且每个线程试图锁定同一个对象但是完全不同的任务,那么你最终可能会遇到死锁和竞争而没有任何真正容易理解的原因。我已经改为代码来反映这个想法,而不是使用一些全局锁。 (我意识到其他人建议全局锁定;我真的不喜欢这个想法,因为其他东西可能会抓住它来执行一些不是这项任务的任务)。
Object MyLock = new Object();
private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
var l = null;
var filestream = null;
Boolean success = false;
if (Monitor.TryEnter(MyLock, 1500))
try {
l = new Logger();
l.LogInformation("Got lock to read/write file-based server state.", (Int32)VipEvent.GotStateLock);
using (fileStream = File.Open(ServerState.PATH, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)){
result = func.Invoke(fileStream);
} //'using' means avoiding the dispose/close requirements
success = true;
}
catch {//your filestream access failed
l.LogInformation("File access failed.", (Int32)VipEvent.ReleasedStateLock);
} finally {
l.LogInformation("About to released state file lock.", (Int32)VipEvent.ReleasedStateLock);
Monitor.Exit(MyLock);//gets you out of the lock you've got
}
} else {
result = null;
//l.LogWarning("Could not get a lock to access the file-based server state.", (Int32)VipEvent.CouldNotGetStateLock);//if the lock doesn't show in the log, then it wasn't gotten; again, if your logger is locking, then you could have some issues here
}
return Success;
}