【问题标题】:C# - Locking issues with MutexC# - 互斥锁的锁定问题
【发布时间】:2009-01-15 21:36:07
【问题描述】:

我有一个 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 设计的。

【问题讨论】:

  • 文件是两个服务器共享的单个文件,还是每个服务器共享一个文件?
  • 你能描述一下调用 Invoke 需要多长时间吗?
  • 您确定没有其他人在 ServerState._lock 上锁定...如果您将其替换为标准锁定模式(在静态对象上),它仍然挂起吗?
  • 嗨丹尼尔,我更新了我的答案来回答你的问题。
  • @sambo99 - 只是用 DateTime 做一个简单的开始/结束比较。现在,它在 0ms 和 15ms 之间交替。是的,我敢肯定——我实际上是从标准锁定模式开始的,然后在挂起时改为互斥锁。

标签: c# asp.net locking mutex


【解决方案1】:

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

虽然互斥锁可以用于 进程内线程同步, 通常首选使用 Monitor, 因为监视器是设计的 专门用于 .NET 框架 从而更好地利用 资源。相比之下,互斥体 类是 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.5 秒,那么下一个 Mutex 将无法抓取它。" - 你确定吗?我认为任何在完成前不到 1.5 秒开始的请求都会失败,否则不会。
  • 我切换到使用 Monitor 而不是 Mutex,我不再遇到问题了。谢谢!
  • 我发现很少有关于锁定的文章提到异常的后果。很高兴你能包含它(锁定模式,这真的是尝试/最终)。从 Mutex 更改为 Monitor 与该问题没有任何关系,它只是降低了出现问题的机会。
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    2) 使用 Monitor 类进行检查,我知道它在 C# 中有效,并且我假设在 ASP.NET 中有效。为此,您可以简单地尝试获取锁,否则优雅地失败。使用它的一种方法是继续尝试获取锁。 (编辑原因:请参阅here;基本上,互斥锁是跨进程的,监视器仅在一个进程中,但专为 .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;
    }
    

    【讨论】:

      猜你喜欢
      • 2022-07-31
      • 2011-08-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-23
      • 2018-05-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多