检查当前线程是否拥有锁

17

假设我有以下代码:

public class SomeClass()
{
    private readonly object _lock = new object();

    public void SomeMethodA()
    {
        lock (_lock)
        {
            SomeHelperMethod();

            //do something that requires lock on _lock
        }
    }

    public void SomeMethodB()
    {
        lock (_lock)
        {
            SomeHelperMethod();

            //do something that requires lock on _lock
        }
    }

    private void SomeHelperMethod()
    {
        lock (_lock)
        {
            //do something that requires lock on _lock
        }
    }
}

SomeHelperMethod中加锁似乎是多余和浪费的,因为所有调用者已经获得了锁。然而,仅仅从 SomeHelperMethod 中删除锁可能是危险的,因为我们以后可能会重构代码并忘记在调用SomeHelperMethod之前对_lock对象加锁。

理想情况下,可以通过在SomeHelperMethod内部断言当前线程拥有_lock锁来解决这个问题:

private void SomeHelperMethod()
{
    Debug.Assert(Monitor.HasLock(_lock));

    //do something that requires lock on _lock
}

但是似乎没有这样的方法。Monitor.TryEnter并没有帮助,因为锁是可重入的。因此,如果当前线程已经拥有锁,则TryEnter仍将成功并返回true。它失败的唯一时间是另一个线程拥有锁且调用超时。

那么,是否存在这样的方法?如果没有,为什么?在我看来,这似乎并不危险,因为它仅告诉您当前线程(而不是另一个线程)是否拥有锁。

10个回答

12

2
太棒了 - 谢谢!根据API文档的判断,它被添加的目的正是我最初想要的: "使用此方法与诊断工具一起使用,例如Assert方法和Contract类,以调试涉及Monitor类的锁定问题。" - Kent Boogaart

5

锁定或重新锁定的成本非常低。这种低成本的缺点是你没有其他同步机制提供的那么多功能。如果必须要锁定时,我会像你之前做的那样直接锁定。

如果你迫切想要省略“不必要”的锁定但又不想承担潜在重构所带来的风险,可以添加注释。如果有人更改了你的代码并且它出现故障,那里就有解释为什么的注释。如果他们还是无法理解,那就是他们的问题而不是你的问题。另一种选择是创建一个用作锁定对象的类,其中包含一个布尔值,可以开启和关闭。然而,由此引入的开销(包括不免费的try/finally块)可能不值得。


基本上你说的是我怀疑的:没有办法做到这一点。虽然不是理想的解决方案,但这是最好的答案。 - Kent Boogaart
更新:但请参考Dave的答案,他指出这在.NET 4.5中现已可用。 - Kent Boogaart
重新锁定应该几乎是免费的。确保锁定肯定不是。它很便宜,但不是免费的。对于锁部分,至少需要一个交错操作,对于解锁部分,还需要另一个交错操作——假设没有竞争。根据 CPU 的不同,这可能意味着大约 100 个周期,甚至在旧的 CPU 上至少是 10 倍。与简单函数调用的典型调用相比,这不是一个巨大的数字,但我认为不能称之为“几乎免费” ;) - Paul Groke

3

你的请求存在一个根本性问题。假设有一个IsLockHeld()方法,你会像这样使用它:

if (not IsLockHeld(someLockObj)) then DoSomething()

这是设计上无法实现的。在if()语句和方法调用之间,你的线程可能会被抢占。允许另一个线程运行并锁定对象。现在,即使锁已经被持有,DoSomething()也将运行。

唯一避免这种情况的方法是尝试获取锁并查看是否成功。使用Monitor.TryEnter()。

在Windows中,同样的模式也存在。除了尝试打开文件,没有其他方法可以检查文件是否被其他进程锁定。原因完全相同。


6
但这并不是我使用它的方式。我将其作为一种断言来使用——一种确认我的代码存在漏洞的方式。如果锁没有被持有,就会出现错误。我不是用它来选择一个代码路径而不是另一个。 - Kent Boogaart
1
是的,你说得对。你需要一个 Monitor.Enter 的包装器,这很丑陋。 - Hans Passant

2
但是似乎没有这样的方法。Monitor.TryEnter不起作用,因为锁是可重入的。因此,如果当前线程已经拥有该锁,则TryEnter仍将成功并返回true。它失败的唯一时间是如果另一个线程拥有锁,并且调用超时。

我找到了一个解决方法,它类似于Monitor.TryEnter(),但没有可重入问题。基本上,你可以使用Monitor.Pulse()代替Monitor.TryEnter()。如果当前线程没有持有锁,则此函数抛出SynchronizationLockException异常。

例如:

try
{
    System.Threading.Monitor.Pulse(lockObj);
}
catch (System.Threading.SynchronizationLockException /*ex*/)
{
    // This lock is unlocked! run and hide ;)
}

文档:http://msdn.microsoft.com/zh-cn/library/system.threading.monitor.pulse.aspx


1
不错,但如果您(或其他人)实际上正在使用Wait()/Pulse()机制进行通信,则可能会产生副作用。 - Sebastian Krysmanski

1

好的,我现在理解了你的问题。 从概念上讲,我现在考虑使用信号量对象。您可以轻松地检查信号量值(大于或等于0的值表示资源可用)。此外,这实际上并没有锁定正在完成的资源,而是通过增加或减少信号量值来完成。 Semaphore对象存在于.NET框架中,但我没有使用它,因此无法提供适当的示例。如果需要,我可以构建一个简单的示例并在此处发布。让我知道。

Alex。

L.E. 我现在不确定您是否实际控制应用程序中的同步机制。如果您只能访问可能被锁定或未锁定的对象,则我的解决方案可能再次无用。


1
信号量API不能实现这种情况。此外,与使用简单监视器相比,这要低得多的效率。 - Kent Boogaart

0

我完全没有.NET编程的经验,但在我的代码(Delphi)中,我曾通过在关键部分类中拥有自定义Acquire()和Release()方法,并使用一个私有成员变量来记录已获取cs的线程id来实现这样的断言。在EnterCriticalSection()之后,将GetCurrentThreadID的返回值写入,而在LeaveCriticalSection()之前,该字段值被重置。

但我不知道.NET是否允许类似的操作,或者你是否能够在你的程序中实现这个...


0

我希望这是可能的,这将使我的代码更加简洁。


0

我有类似的要求,需要检查当前线程是否锁定了锁,因此我可以像这样做。

public void Method1()
{
    if(!lockHeld()) lock();
    //DO something
}
public void Method2()
{
    if(!lockHeld()) lock();
    //DO something
}
public void Method3()
{
    if(!lockHeld()) lock();

    //Do something

    unlock()
}

所以我为此编写了一个类:

public class LockObject
{
    private Object internalLock;

    private bool isLocked;
    public bool IsLocked
    {
        get { lock (internalLock) return isLocked; }
        private set { lock (internalLock) isLocked = value; }
    }

    public LockObject()
    {
        internalLock = new object();
    }

    public void UsingLock(Action method)
    {
        try
        {
            Monitor.Enter(this);
            this.IsLocked = true;

            method();
        }
        finally
        {
            this.IsLocked = false;
            Monitor.Exit(this);
        }
    }
}

然后我可以这样使用它:

    public void Example()
    {
        var obj = new LockObject();

        obj.UsingLock(() =>
        {
            //Locked, and obj.IsLocked = true
        });
    }

注意:我在这个类中省略了Lock() Unlock()方法,但它们基本上只是设置IsLocked为true的Monitor.Enter/Exit的包装器。这个例子只是说明它在风格上与lock()非常相似。
可能是不必要的,但对我很有用。

0

如果您需要在.NET 4.5下使用,请参考@Dave的答案。

但是,如果您需要在.NET 3.5或4下使用,则可以将Monitor锁的使用替换为ReaderWriterLockSlim,如下所示。

根据Joeseph Albahari的说法,这将大约使您的(无争用)锁定开销翻倍,但仍然非常快(估计40纳秒)。 (免责声明,该页面未说明测试是否使用了递归锁定策略。)

public class SomeClass()
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

public void SomeMethodA()
{
    _lock.EnterWriteLock();
    try
    {
        SomeHelperMethod();

        //do something that requires lock on _lock
    }
    finally
    {
       _lock.ExitWriteLock();
    }
}

private void SomeHelperMethod()
{
    Debug.Assert(_lock.IsWriteLockHeld);

    //do something that requires lock on _lock
}
}

如果您觉得这太低效了,当然可以编写自己的包装类来封装 Monitor 锁。但是,其他帖子提到了一个布尔标志,这是误导性的,因为问题不是“锁是否被任何线程持有?”(这是一个愚蠢和危险的问题)。正确的问题是“此线程是否持有锁?”没问题,只需将“标志”设置为 ThreadLocal 计数器(用于递归锁支持),并确保在 Monitor.Enter 之后和 Monitor.Exit 之前进行更新。作为改进,断言计数器在“解锁”时不会下降到 0,因为这将表明 Enter/Exit 调用不平衡。

包装类的另一个原因是避免有人滑入 _lock.EnterReadLock 调用。根据类的使用情况,这些调用可能实际上是有帮助的,但令人惊讶的是,许多程序员不理解读写锁。


-1

这不是最好的做法,但是...

bool OwnsLock(object someLockObj)
{
  if (Monitor.TryEnter(someLockObj))
  {
    Monitor.Exit();
    return false;
  }
  return true;
}

Debug.Assert(OwnsLock(_someLockObject), "_someLockObject should be locked when this method is called")

1
锁定是可重入的,因此此代码无法工作。即使调用代码已经拥有锁定,监视器也将成功地获取锁定。 - Kent Boogaart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接