ReaderWriterLockSlim.EnterUpgradeableReadLock()总是导致死锁吗?

9

我非常熟悉ReaderWriterLockSlim,但最近在一个类中尝试实现EnterUpgradeableReadLock()时...很快我意识到当2个或更多线程运行代码时,这几乎肯定会造成死锁:

Thread A --> enter upgradeable read lock
Thread B --> enter upgradeable read lock
Thread A --> tries to enter write lock, blocks for B to leave read
Thread B --> tries to enter write lock, blocks for A to leave read
Thread A --> waiting for B to exit read lock
Thread B --> waiting for A to exit read lock

我在这里漏掉了什么?

编辑

添加了我的情况的代码示例。 Run() 方法将由2个或更多线程同时调用。

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

    public void Run()
    {
        _lock.EnterUpgradeableReadLock();
        try
        {
            _lock.EnterWriteLock();
            try
            {
                // Do something
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();
        }
    }
}

你能否发布一些简单的代码来复制这个问题? - Gusdor
@Gusdor 确定,正在更新。 - Haney
在任何给定的时间,只有一个线程可以进入可升级模式。 - Jon B
@JonB,那它与写锁相比有什么意义?它存在的理由是什么? - Haney
当您只有一个可升级的线程时,它似乎仍具有价值。如果您有多个可能需要写入的线程,则不是最佳选择。您可以输入读取、退出,然后输入写入。然后两个线程中的一个将获胜。 - Jon B
显示剩余3条评论
3个回答

38

虽然是OP之后很久了,但我不同意目前接受的答案。

Thread B --> enter upgradeable read lock这个说法是不正确的。根据文档

同时只有一个线程可以处于可升级模式

而且针对您的评论所回应的是:它用途非常不同于读写模式。

TL;DR(简而言之)。可升级模式很有用:

  • 如果一个写入者必须在写入共享资源之前检查它,并且(可选地)需要避免与其他写入者产生竞争条件;
  • 并且它不应该停止读者,直到它完全确定必须写入共享资源;
  • 并且在进行检查后,写入者很可能会决定不写入共享资源。

或者,在伪代码中,它看起来像这样:

// no other writers or upgradeables allowed in here => no race conditions
EnterUpgradeableLock(); 
if (isWriteRequired()) { EnterWriteLock(); DoWrite(); ExitWriteLock(); } 
ExitUpgradeableLock();

相较于这个,提供更好的性能÷

EnterWriteLock(); if (isWriteRequired()) { DoWrite(); } ExitWriteLock();

如果独占锁的部分使用了SpinLock,那么就需要小心谨慎地使用。


类似的锁结构

可升级锁与SQL Server SIX锁(共享且带有独占意图)非常相似。

  • 使用这些术语重写上述语句,可升级锁表示“写操作想要写入资源,但在独占锁并执行写操作之前,希望与其他读取器共享资源并检查条件是否需要执行独占锁”

如果没有意向锁,您必须在独占锁内执行“我应该进行此更改”检查,这可能会损害并发性。

为什么不能共享可升级锁?

如果可升级锁可以与其他可升级锁共享,则可能会与其他可升级锁所有者发生竞争条件。因此,进入写锁定后,您需要进行另一个检查,从而消除了在第一次读取时进行检查的好处。

例子

如果将所有锁等待/进入/退出事件视为顺序,并将锁内部工作视为并行,则可以用“大理石”形式(e 进入;w 等待;x 退出;cr 检查资源;mr 更改资源;R 共享/读取;U 意向/可升级;W 独占/写)编写一个场景:

1--eU--cr--wW----eW--mr--xWxU--------------
2------eR----xR----------------eR--xR------
3--------eR----xR--------------------------
4----wU----------------------eU--cr--xU----

描述:

T1进入可升级/意向锁。T4等待可升级/意向锁。T2和T3进入读锁。T1检查资源,赢得比赛并等待独占/写锁。T2&T3退出他们的锁。T1进入独占/写锁并进行更改。T4进入可升级/意向锁,不需要进行更改且退出,而不会阻止同时进行另一个读取的T2。

用8个要点描述...

可升级锁是:

  1. 任何Writer都可以使用;
  2. 如果有任何原因(输掉竞争条件或在Getsert模式下)首先检查然后决定不执行写入操作的操作,
  3. 并且不应该阻止Reader直到它知道必须执行Write操作;
  4. 此时将取出独占锁并执行操作。

如果以下情况之一适用,则不需要可升级锁(包括但不限于):

  1. 读者和写者之间的争用率是接近零的(写入条件检查非常快),即可升级结构不能帮助Reader吞吐量;
  2. 具有写入锁的编写器只写入一次的概率为~1,因为以下任一情况:

    • ReadLock-Check-WriteLock-DoubleCheck是如此快,它仅导致每兆写入之一的竞争输家;
    • 所有更改都是唯一的(所有更改都必须发生,无法存在竞争);
    • "最后更改获胜"(所有更改仍然必须发生,即使它们不是唯一的)

如果 lock(...){...} 更合适,则也不需要可升级锁,例如:

  1. 重叠读和/或写窗口的概率较低(锁可以防止非常罕见的事件,而不仅仅是保护高度可能的事件,更不用说简单的内存屏障要求了);
  2. 您所有的锁获取都是可升级或写入,从不读取('duh')

÷ 其中“性能”由您定义

如果将锁对象视为表,将受保护的资源视为层次结构中较低的资源,则此类比约束大致成立

在读取锁中的初始检查是可选的,在可升级锁中的检查是强制性的,因此它可以用于单个或双重检查模式。


1
太棒了的回答! - Iucounu

1

1
我想我明白了。这是一种第三种“类型”的锁,它尊重N次读取,但只允许一个线程随时进入可升级状态...我猜当单个线程进行写操作时很有用,但仍然不完全不同于读取...写入模式? - Haney
不确定为什么我从未将其标记为答案。谢谢! - Haney

-1

你的示例中有错误

private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

它应该是这样的

private static readonly ReaderWriterLockSlim _lock = new  ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

现在,在您的代码中,每次实例化一个类时,它都会创建一个新的ReaderWriterLockSlim实例,但由于每个线程都有自己的实例,因此无法锁定任何内容。将其设置为静态将强制所有线程使用一个实例,这将按预期工作。

1
这并不一定是正确的。如果我在另一个类中保留了该类的静态或单例引用,它将按预期运行。 - Haney
1
OP的代码死锁是最好的证明,表明多个(自称)作者正在使用同一个锁实例,证明了你证明他错的错误。 - Evgeniy Berezovsky

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