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();
        }
    }
}
4

3 回答 3

37

OP 之后很长时间,但我不同意当前接受的答案。

该说法Thread B --> enter upgradeable read lock 不正确。从文档

任何时候只有一个线程可以处于可升级模式

并回应您的评论:它旨在用于与读写模式非常不同的用途。

TL;博士。可升级模式很有用:

  • 如果作者在写入之前必须检查共享资源,并且(可选)需要避免与其他作者的竞争条件;
  • 并且在 100% 确定它必须写入共享资源之前,它不应该阻止读者;
  • 一旦执行了检查,作者很可能会决定不写入共享资源

或者,在伪代码中,这里:

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

给出“更好的性能” ÷比这:

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

如果独占锁定部分由于使用SpinLock而需要很长时间,则应谨慎使用。


类似的锁结构

Upgradeable lock与 SQL server SIX lock (Shared with Intent to go eXclusive) 惊人地相似。

  • 为了用这些术语重写上面的语句,可升级锁表示“写入者打算写入资源,但希望与其他读取者共享它,同时它 [双重] 检查条件以查看它是否应该排他地锁定并执行写入“

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

为什么不能分享升级版?

如果可升级锁可与其他可升级锁共享,则可能会与其他可升级锁所有者发生竞争。因此,您将需要在写入锁内再次进行检查,从而消除执行检查的好处,而不会首先阻止其他读取。

例子

如果我们将所有锁等待/进入/退出事件视为顺序事件,并且将锁内的工作视为并行,那么我们可以以“大理石”形式编写场景(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 进入 Upgradeable/Intent 锁,不需要更改并退出,同时不会阻塞 T2,T2 同时进行另一次读取。

在 8 个要点中...

可升级锁是:

  1. 由任何作家使用;
  2. 谁可能先检查,然后出于任何原因决定不执行写入(失去竞争条件,或在 Getsert 模式中);
  3. 谁不应该阻止读者,直到它知道它必须执行写;
  4. 然后它将取出一个排他锁并这样做。

如果满足以下条件之一(包括但不限于),则不需要升级:

  1. 读取器和写入器之间的争用率writelock-check-nowrite-exit大约为零(写入条件检查非常快) - 即可升级的构造不会帮助读取器吞吐量;
  2. 一个写入器在写入锁中写入一次的概率约为 1,因为:

    • ReadLock-Check-WriteLock-DoubleCheck速度如此之快,每万亿次写入只会导致一次比赛失败;
    • 所有变化都是独一无二的(所有变化都必须发生,种族不能存在);或者
    • “最后的更改获胜”(所有更改仍然必须发生,即使它们不是唯一的)

如果 alock(...){...}更合适,也不需要,即:

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

÷ 其中“性能”由您来定义

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

Read lock 中的初始检查是可选的,Upgradeable lock 中的检查是强制性的,因此它可以用于单一或双重检查模式。

于 2014-10-26T21:18:16.490 回答
1

我建议避免使用 EnterUpgradeableReadLock()。只需使用 EnterWriteLock() 代替。我知道这似乎效率低下,无论如何,可升级的读锁几乎和写锁一样糟糕。

于 2014-01-28T16:26:56.773 回答
-1

您的示例中有错误

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

它应该是

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

现在,在您的代码中,每次实例化一个类时,它都会创建 ReaderWriterLockSlim 的新实例,该实例无法锁定任何内容,因为每个线程都有它自己的实例。使其成为静态将强制所有线程使用一个实例,该实例将正常工作

于 2017-04-25T04:21:52.993 回答