ReaderWriterLockSlim 啥时候比简单的锁更好?

Posted

技术标签:

【中文标题】ReaderWriterLockSlim 啥时候比简单的锁更好?【英文标题】:When is ReaderWriterLockSlim better than a simple lock?ReaderWriterLockSlim 什么时候比简单的锁更好? 【发布时间】:2011-05-12 04:05:38 【问题描述】:

我正在使用这段代码在 ReaderWriterLock 上做一个非常愚蠢的基准测试,其中读取发生的频率是写入的 4 倍:

class Program

    static void Main()
    
        ISynchro[] test =  new Locked(), new RWLocked() ;

        Stopwatch sw = new Stopwatch();

        foreach ( var isynchro in test )
        
            sw.Reset();
            sw.Start();
            Thread w1 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w1.Start( isynchro );

            Thread w2 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w2.Start( isynchro );

            Thread r1 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r1.Start( isynchro );

            Thread r2 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r2.Start( isynchro );

            w1.Join();
            w2.Join();
            r1.Join();
            r2.Join();
            sw.Stop();

            Console.WriteLine( isynchro.ToString() + ": " + sw.ElapsedMilliseconds.ToString() + "ms." );
        

        Console.WriteLine( "End" );
        Console.ReadKey( true );
    

    static void ReadThread(Object o)
    
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 500; i++ )
        
            Int32? value = synchro.Get( i );
            Thread.Sleep( 50 );
        
    

    static void WriteThread( Object o )
    
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 125; i++ )
        
            synchro.Add( i );
            Thread.Sleep( 200 );
        
    



interface ISynchro

    void Add( Int32 value );
    Int32? Get( Int32 index );


class Locked:List<Int32>, ISynchro

    readonly Object locker = new object();

    #region ISynchro Members

    public new void Add( int value )
    
        lock ( locker ) 
            base.Add( value );
    

    public int? Get( int index )
    
        lock ( locker )
        
            if ( this.Count <= index )
                return null;
            return this[ index ];
        
    

    #endregion
    public override string ToString()
    
        return "Locked";
    


class RWLocked : List<Int32>, ISynchro

    ReaderWriterLockSlim locker = new ReaderWriterLockSlim();

    #region ISynchro Members

    public new void Add( int value )
    
        try
        
            locker.EnterWriteLock();
            base.Add( value );
        
        finally
        
            locker.ExitWriteLock();
        
    

    public int? Get( int index )
    
        try
        
            locker.EnterReadLock();
            if ( this.Count <= index )
                return null;
            return this[ index ];
        
        finally
        
            locker.ExitReadLock();
        
    

    #endregion

    public override string ToString()
    
        return "RW Locked";
    

但我知道两者的表现或多或少是相同的:

Locked: 25003ms.
RW Locked: 25002ms.
End

即使读取频率是写入频率的 20 倍,性能仍然(几乎)相同。

我在这里做错了吗?

亲切的问候。

【问题讨论】:

顺便说一句,如果我取消睡眠:“锁定:89 毫秒。RW 锁定:32 毫秒。” - 或增加数字:“锁定:1871 毫秒。RW 锁定:2506 毫秒。”。 如果我删除睡眠,locked 比 rwlocked 快 这是因为您正在同步的代码太快而无法创建任何争用。请参阅 Hans 的回答和我的编辑。 【参考方案1】:

ReaderWriterLockSlim 什么时候比简单的锁更好?

当您的读取次数明显多于写入次数时。

【讨论】:

【参考方案2】:

如果您锁定需要较长时间执行的部分代码,使用ReaderWriterLockSlim 将获得比简单锁定更好的性能。在这种情况下,读者可以并行工作。获取ReaderWriterLockSlim 比输入简单的Monitor 需要更多时间。检查我的ReaderWriterLockTiny 实现的读写器锁,它比简单的锁语句更快,并提供了读写器功能:http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/

【讨论】:

我喜欢这个。这篇文章让我想知道 Monitor 在每个平台上是否都比 ReaderWriterLockTiny 快? 虽然 ReaderWriterLockTiny 比 ReaderWriterLockSlim(和 Monitor)快,但有一个缺点:如果有很多读者需要相当长的时间,它永远不会给作者机会(他们几乎会无限期地等待)。另一方面,ReaderWriterLockSlim 设法平衡读者/作者对共享资源的访问。【参考方案3】:

我自己的测试表明,ReaderWriterLockSlim 的开销大约是普通lock 的 5 倍。这意味着 RWLS 要优于普通的旧锁,通常会发生以下情况。

读者数量明显多于作者。 锁必须保持足够长的时间才能克服额外的开销。

在大多数实际应用中,这两个条件不足以克服额外的开销。特别是在您的代码中,锁的持有时间如此之短,以至于锁开销可能是主要因素。如果您将这些 Thread.Sleep 调用移动到锁内,那么您可能会得到不同的结果。

【讨论】:

【参考方案4】:

编辑 2:只需从 ReadThreadWriteThread 中删除 Thread.Sleep 调用,我发现 Locked 的性能优于 RWLocked。我相信Hans在这里一针见血;你的方法太快了,不会产生争用。当我将Thread.Sleep(1) 添加到LockedRWLockedGetAdd 方法(并使用4 个读取线程对抗1 个写入线程)时,RWLocked 击败了Locked


编辑:好的,如果我在第一次发布此答案时真的在思考,我至少会意识到为什么你提出那里的Thread.Sleep 调用:您试图重现读取比写入更频繁的场景。这不是正确的方法。相反,我会为您的 AddGet 方法引入额外的开销,以创建更大的争用机会(如 Hans suggested),创建比写入线程更多的读取线程(以确保读取比写入更频繁),并删除来自ReadThreadWriteThreadThread.Sleep 调用(实际上减少 争用,实现与您想要的相反)。


我喜欢你目前所做的。但这里有一些我马上就看到的问题:

    为什么会调用Thread.Sleep?这些只是将您的执行时间增加了一个常数,这会人为地使性能结果收敛。 我也不会在您的Stopwatch 测量的代码中包含新Thread 对象的创建。这不是一个容易创建的对象。

解决上述两个问题后,您是否会看到显着差异,我不知道。但我认为应该在讨论继续之前解决这些问题。

【讨论】:

+1:#2 上的好点——它确实会扭曲结果(每次之间的差异相同,但比例不同)。虽然如果你运行足够长的时间,创建Thread 对象的时间将开始变得微不足道。【参考方案5】:

这个程序没有争用。 Get 和 Add 方法在几纳秒内执行。多个线程在确切时间访问这些方法的几率非常小。

在它们中调用 Thread.Sleep(1) 并从线程中移除睡眠以查看差异。

【讨论】:

【参考方案6】:

除非您拥有多核硬件(或至少与您计划的生产环境相同),否则您将无法在此处进行实际测试。

更明智的测试是通过在锁内部放置一个短暂的延迟来延长锁定操作的生命周期。这样,您应该真正能够对比使用ReaderWriterLockSlim 添加的并行性与基本lock() 隐含的序列化。

目前,您的锁定操作所花费的时间会在锁外发生的睡眠调用产生的噪音中丢失。两种情况下的总时间主要与睡眠有关。

您确定您的实际应用将具有相同数量的读取和写入吗? ReaderWriterLockSlim 非常适合您有很多读者和相对不频繁的作者的情况。 1 个写入线程与 3 个读取线程应该更好地展示 ReaderWriterLockSlim 的好处,但无论如何您的测试应该符合您预期的实际访问模式。

【讨论】:

【参考方案7】:

无竞争锁的获取时间为微秒级,因此您对Sleep 的调用将使您的执行时间相形见绌。

【讨论】:

【参考方案8】:

在您的示例中,睡眠意味着通常没有争用。非竞争锁非常快。为此,您需要一个 contended 锁;如果该争用中有写入,它们应该大致相同(lock 甚至可能更快) - 但如果是 大部分 读取(很少有写入争用),我希望ReaderWriterLockSlim 锁的性能优于lock

就个人而言,我更喜欢这里的另一种策略,使用引用交换 - 因此读取可以始终读取而无需检查/锁定 / 等。写入将更改更改为 克隆 副本,然后使用 Interlocked.CompareExchange交换引用(如果另一个线程在此期间改变了引用,则重新应用他们的更改)。

【讨论】:

Copy-and-swap-on-write 绝对是这里的最佳选择。 非锁定copy-and-swap-on-write完全避免了资源争用或读者锁定;在没有争用的情况下,它对编写者来说很快,但在存在争用的情况下可能会严重阻塞日志。让编写者在制作副本之前获取锁(读者不会关心锁)并在执行交换后释放锁可能会很好。否则,如果 100 个线程每个都尝试一次同时更新,在最坏的情况下,它们可能必须平均每个执行大约 50 次复制更新尝试交换操作(总共 5,000 次复制更新尝试交换操作来执行 100 次更新)。 我认为它应该是这样的,如果有错误请告诉我:bool bDone=false; while(!bDone) object origObj = theObj; object newObj = origObj.DeepCopy(); // then make changes to newObj if(Interlocked.CompareExchange(ref theObj, tempObj, newObj)==origObj) bDone=true; @mcmillab 基本上,是的;在一些简单的情况下(通常是在包装单个值时,或者如果以后写覆盖他们从未见过的东西并不重要),您甚至可以摆脱Interlocked 并仅使用volatile 字段和只是分配给它 - 但如果您需要确定您的更改没有完全丢失,那么Interlocked 是理想的 @jnm2:这些集合经常使用 CompareExchange,但我认为它们不会进行大量复制。 CAS+lock 不一定是多余的,如果有人使用它来考虑并非所有编写者都会获得锁的可能性。例如,一个资源的争用通常很少,但有时可能很严重,它可能有一个锁和一个指示作者是否应该获取它的标志。如果标志是明确的,作者只需做一个 CAS,如果成功,他们就会继续前进。但是,如果 CAS 失败两次以上的编写器可以设置该标志;然后标志可以保持设置...【参考方案9】:

查看这篇文章:http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx

您的睡眠时间可能足够长,以至于您的锁定/解锁在统计上并不显着。

【讨论】:

此链接已不存在 - 我建议更新答案或将其删除。【参考方案10】:

我想这是因为你的读者和作者线程中的睡眠。 您的读取线程有 500 次 50 毫秒的睡眠时间,即 25000 大部分时间都在睡眠中

【讨论】:

以上是关于ReaderWriterLockSlim 啥时候比简单的锁更好?的主要内容,如果未能解决你的问题,请参考以下文章

读写锁-ReaderWriterLockSlim

C# 多线程锁之ReaderWriterLockSlim

线程同步-使用ReaderWriterLockSlim类

ReaderWriterLockSlim使用示例

C# ReaderWriterLockSlim类

C# - 为啥在使用 ReaderWriterLockSlim 时出现 LockRecursionException?