3

我有一个 Web 应用程序,它控制哪些 Web 应用程序从我们的负载均衡器获取流量。Web 应用程序在每个单独的服务器上运行。

它跟踪处于 ASP.NET 应用程序状态的对象中每个应用程序的“输入或输出”状态,并且只要状态发生更改,该对象就会被序列化为磁盘上的一个文件。当 Web 应用程序启动时,状态会从文件中反序列化。

虽然该站点本身只收到几个请求,而且它很少访问该文件,但我发现由于某种原因在尝试读取或写入文件时很容易发生冲突。这种机制需要非常可靠,因为我们有一个自动化系统,定期对服务器进行滚动部署。

在任何人发表任何质疑上述任何谨慎性的评论之前,请允许我简单地说,解释其背后的原因会使这篇文章比现在长得多,所以我想避免搬山。

也就是说,我用来控制对文件的访问的代码如下所示:

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 事件中实例化。在声明中静态实例化它时,我得到相同的结果。

借口,借口:线程/锁定不是我的强项,因为我通常不必担心它。

关于为什么它随机无法获得信号的任何建议?


更新:

我已经添加了正确的错误处理(多么令人尴尬!),但我仍然遇到相同的错误 - 并且为了记录,未处理的异常从来都不是问题。

只有一个进程会访问该文件——我没有为此应用程序的网络池使用网络花园,也没有其他应用程序使用该文件。我能想到的唯一例外是当应用程序池回收时,旧的 WP 在创建新的 WP 时仍处于打开状态 - 但我可以从任务管理器中看出问题发生在只有一个工作进程时。

@mmr:使用 Monitor 与使用 Mutex 有何不同?根据 MSDN 文档,它看起来好像在有效地做同样的事情——如果我不能用我的 Mutex 获得锁,它通过返回 false 来优雅地失败。

另一件需要注意的事情:我遇到的问题似乎完全是随机的 - 如果它在一个请求上失败,它可能会在下一个请求上正常工作。似乎也没有模式(至少肯定没有其他模式)。


更新 2:

此锁不用于任何其他调用。在 InvokeOnFile 方法之外引用 _lock 的唯一时间是在实例化它时。

调用的 Func 要么从文件中读取并反序列化为对象,要么将对象序列化并将其写入文件。这两个操作都不是在单独的线程上完成的。

ServerState.PATH 是一个静态只读字段,我不希望它会导致任何并发问题。

我还想重申我之前的观点,即我无法在本地(在 Cassini 中)重现这一点。


得到教训:

  • 使用正确的错误处理(呃!)
  • 为工作使用正确的工具(并对工具的作用/方式有基本的了解)。正如 sambo 指出的那样,使用 Mutex 显然有很多开销,这导致我的应用程序出现问题,而 Monitor 是专门为 .NET 设计的。
4

3 回答 3

16

只有在需要跨进程同步时才应该使用互斥锁。

虽然互斥锁可用于进程内线程同步,但通常首选使用 Monitor,因为 Monitor 是专门为 .NET Framework 设计的,因此可以更好地利用资源。相反,Mutex 类是 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
于 2009-01-15T21:40:26.513 回答
2

如果某些文件操作失败,锁将不会被释放。很可能就是这种情况。将文件操作放在 try/catch 块中,并在 finally 块中释放锁。

无论如何,如果您在 Global.asax Application_Start 方法中读取文件,这将确保没有其他人在处理它(您说文件是在应用程序启动时读取的,对吗?)。为了避免应用程序池重启等冲突,您可以尝试读取文件(假设写操作需要排他锁),然后等待1秒,如果抛出异常则重试。

现在,您遇到了同步写入的问题。无论哪种方法决定更改文件,都应该注意不要调用写操作,如果另一个正在使用简单的锁定语句进行。

于 2009-01-15T21:47:02.770 回答
0

我在这里看到了几个潜在的问题。

编辑更新 2:如果该函数是一个简单的序列化/反序列化组合,我会将两者分成两个不同的函数,一个是“序列化”函数,一个是“反序列化”函数。他们真的是两个不同的任务。然后,您可以执行不同的特定于锁定的任务。Invoke 很漂亮,但是我自己为了“漂亮”而不是“工作”而遇到了很多麻烦。

1)您的 LogInformation 功能是否锁定?因为你首先在互斥锁内部调用它,然后一旦你释放互斥锁。因此,如果存在写入日志文件/结构的锁,那么您最终可能会遇到竞争条件。为避免这种情况,请将日志放在锁内。

2) 检查使用 Monitor 类,我知道它在 C# 中工作,我假设在 ASP.NET 中工作。为此,您可以简单地尝试获取锁,否则优雅地失败。使用它的一种方法是继续尝试获取锁。(编辑原因:请参见此处;基本上,互斥锁是跨进程的,监视器仅在一个进程中,但是是为 .NET 设计的,因此是首选。文档没有给出其他真正的解释。)

3)如果文件流打开失败,因为别人有锁,会发生什么?这将引发异常,并可能导致此代码表现不佳(即,锁仍由具有异常的线程持有,并且另一个线程可以获取它)。

4)函数本身呢?这会启动另一个线程,还是完全在一个线程内?访问 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;
}
于 2009-01-15T21:53:08.937 回答