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


80

我用这段代码在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倍,性能仍然(几乎)相同。

我在这里做错什么了吗?

亲切的问候。


3
顺便说一句,如果我睡不着觉:“锁定:89ms。RW锁定:32ms。”-或增加数字:“锁定:1871ms。RW锁定:2506ms。”
马克·格雷韦尔

1
如果我取消睡眠,锁定比rwlocked快
vtortola

1
这是因为您正在同步的代码太快而无法创建任何争用。参见汉斯的答案和我的编辑。
丹涛

Answers:


107

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

就我个人而言,我更喜欢在这里使用引用交换的另一种策略-因此读操作始终可以在不进行检查/锁定/等操作的情况下进行。写操作将其更改为克隆副本,然后用于Interlocked.CompareExchange交换引用(如果有另一个线程,则重新应用其更改)在此期间对参考进行了更改)。


1
写入复制和交换绝对是解决问题的方法。
LBushkin

24
非锁定写时复制和交换完全避免了资源争用或读者锁定。在没有争用的情况下,这对于作者来说速度很快,但是在存在争用的情况下,可能会严重阻塞日志。最好让作家在复制前获得一个锁(读者不在乎该锁),然后在进行交换后释放该锁。否则,如果每个线程尝试同时进行100个更新,则在最坏的情况下,每个线程可能平均必须执行约50个copy-update-try-swap操作(总共5,000个copy-update-try-swap操作才能执行100个更新)。
超级猫

1
这是我认为的样子,如果有错,请告诉我: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 2013年

1
基本上是@mcmillab,是的;在某些简单的情况下(通常在包装单个值时使用,或者以后再写他们从未见过的东西都无关紧要的情况下),您甚至可以避免丢失,Interlocked而只使用一个volatile字段并对其进行赋值-但是如果您需要确保所做的更改不会完全丢失,那就Interlocked比较理想了
Marc Gravell

3
@ jnm2:这些集合经常使用CompareExchange,但我认为它们并没有进行大量复制。CAS +锁不一定是多余的,如果使用它来允许并非所有编写者都将获得锁的可能性。例如,争用通常很少见但有时可能很严重的资源,可能有一个锁和一个标志,指示作者是否应该获取它。如果标记清楚,那么编写者只需执行CAS,如果成功,就会继续前进。但是,使CAS失败两次以上的编写器可以设置该标志。然后可以设置该标志...
supercat

23

我自己的测试表明,ReaderWriterLockSlim其开销是正常值的5倍lock。这意味着RWLS要胜过普通的旧锁,通常会发生以下情况。

  • 读者人数大大超过了作家。
  • 锁必须保持足够长的时间才能克服额外的开销。

在大多数实际应用中,这两个条件不足以克服额外的开销。具体来说,在您的代码中,锁的保留时间很短,以至于锁开销可能是主要因素。如果要Thread.Sleep在锁中移动这些呼叫,则可能会得到不同的结果。


17

该程序没有争用。Get和Add方法在几纳秒内执行。多个线程在准确的时间命中那些方法的几率很小。

在其中放置一个Thread.Sleep(1)调用,并从线程中删除睡眠以查看区别。


12

编辑2:只需删除Thread.Sleep来自ReadThread和的电话WriteThread,我就看到了Locked表现出色RWLocked。我相信汉斯会在这里伤到头。您的方法太快,无法引起竞争。当我加入Thread.Sleep(1)GetAdd的方法LockedRWLocked(和使用4读出针对1个写入线程的线程),RWLocked击败裤子的Locked


编辑:好的,如果我实际上是,当我第一次张贴了这个答案,我就已经至少实现了,为什么你把Thread.Sleep在那里呼叫:您试图重现的读比写更经常发生的情况。这不是正确的方法。相反,我会给您AddGet方法增加额外的开销,以产生更大的争用机会(如Hans所建议),创建比写入线程更多的读取线程(以确保读取次数比写入线程更多),并删除Thread.Sleep来自ReadThread和的调用WriteThread(实际上减少竞争,实现您想要的目标)。


我喜欢你到目前为止所做的。但是,我立即看到了一些问题:

  1. 为什么要Thread.Sleep打电话?这些只是使您的执行时间膨胀了一定的数量,这将人为地使性能结果收敛。
  2. 我也不会Thread在您的衡量的代码中包括新对象的创建Stopwatch。那不是要创建的琐碎对象。

解决以上两个问题后,您是否会发现明显的不同,我不知道。但是我认为应该在讨论继续之前解决它们。


+1:#2-的好点-它确实会使结果偏斜(每次之间的差异相同,但比例不同)。尽管如果运行足够长的时间,创建Thread对象的时间将开始变得微不足道。
尼尔森·罗瑟梅尔

10

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


我喜欢这个。这篇文章让我想知道Monitor在每个平台上是否比ReaderWriterLockTiny快?
jnm2 2015年

虽然ReaderWriterLockTiny的速度比ReaderWriterLockSlim(以及Monitor)快,但有一个缺点:如果有很多读者需要很长时间,那么它就永远不会给写作者任何机会(他们几乎可以无限期地等待)。平衡读者/作家对共享资源的访问。
tigrou


3

毫无争议的锁需要花费微秒级的时间,因此您对的调用将使您的执行时间相形见Sleep


3

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

一个更明智的测试是通过在锁内部放置短暂的延迟来延长锁操作的寿命。这样一来,您实际上应该能够将使用添加的并行性ReaderWriterLockSlim与basic隐含的序列化进行对比lock()

当前,锁定操作所花费的时间因锁定之外发生的Sleep调用所产生的噪音而丢失。两种情况下的总时间大部分与睡眠有关。

您确定您的真实应用程序具有相同数量的读取和写入吗? ReaderWriterLockSlim如果您有许多读者和相对较少的作家,那真的更好。1个写程序线程与3个读程序线程应该ReaderWriterLockSlim更好地证明好处,但是在任何情况下,您的测试都应符合预期的实际访问模式。


2

我猜这是因为您的读取器和写入器线程处于睡眠状态。
您的读取线程有500tims 50ms睡眠,这是大多数情况下的25000睡眠


0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.