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

时间:2010-11-18 16:51:58

标签: c# .net multithreading locking

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

我在这里做错了吗?

亲切的问候。

10 个答案:

答案 0 :(得分:95)

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

就个人而言,我更喜欢这里的另一种策略,使用引用交换 - 所以读取总是可以在不检查/锁定等的情况下读取。写入将其更改为克隆副本,然后使用{{1交换引用(如果另一个线程在临时中突变了引用,则重新应用它们的更改)。

答案 1 :(得分:21)

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

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

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

答案 2 :(得分:14)

此计划中没有争议。 Get和Add方法在几纳秒内执行。多个线程在确切时间点击这些方法的几率非常小。

将Thread.Sleep(1)调用放入其中并从线程中移除睡眠以查看差异。

答案 3 :(得分:10)

修改2 :只需移除Thread.SleepReadThread的{​​{1}}来电,我看到WriteThread的效果优于Locked。我相信Hans在这里击中了钉子;你的方法太快,不会产生争用。当我将RWLocked添加到Thread.Sleep(1)Get的{​​{1}}和Add方法时(并且针对1个写线程使用了4个读取线程),{{1}从Locked打败裤子。


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


我喜欢你到目前为止所做的一切。但是这里有一些我不知所措的问题:

  1. 为什么Thread.Sleep来电?这些只会使您的执行时间膨胀一定量,这将人为地使性能结果收敛。
  2. 我也不会在您的ReadThread测量的代码中包含新WriteThread个对象的创建。这不是一个简单的创造对象。
  3. 一旦解决上述两个问题,您是否会看到重大差异,我不知道。但我相信在讨论继续之前应该解决它们。

答案 4 :(得分:8)

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

答案 5 :(得分:3)

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

你的睡眠时间可能足够长,以致你的锁定/解锁在统计上无关紧要。

答案 6 :(得分:3)

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

答案 7 :(得分:3)

除非您拥有多核硬件(或者至少与您计划的生产环境相同),否则您将无法获得真实的测试。

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

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

您确定您的真实应用会有相同数量的读写吗?对于你有很多读者和相对不常见的作家的情况,ReaderWriterLockSlim真的更好。 1个作者线程与3个读者线程应该更好地展示ReaderWriterLockSlim的好处,但无论如何,您的测试应该与您预期的实际访问模式相匹配。

答案 8 :(得分:2)

我想这是因为你在读者和作家的线程中都有睡眠 你的阅读帖子有500个50ms的睡眠,25000 大部分时间都在睡觉

答案 9 :(得分:0)

  

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

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