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

vto*_*ola 74 .net c# 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";
    }
}
Run Code Online (Sandbox Code Playgroud)

但我认为两者的表现方式大致相同:

Locked: 25003ms.
RW Locked: 25002ms.
End
Run Code Online (Sandbox Code Playgroud)

即使写入的读取次数增加20倍,性能仍然(几乎)相同.

我在这里做错了吗?

亲切的问候.

Mar*_*ell 97

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

就个人而言,我更喜欢这里的另一种策略,使用引用交换 - 所以读取总是可以在没有检查/锁定等的情况下读取.写入使其更改为克隆副本,然后用于Interlocked.CompareExchange交换引用(如果另一个线程重新应用它们的更改)在过渡期间改变了参考文献).

  • 非锁定的copy-and-swap-on-write完全避免了读者的资源争用或锁定; 当没有争用时,它对作者来说很快,但在争用的情况下可能会变得严重阻塞.在编写副本之前让编写者获得锁定(读者不关心锁定)并在执行交换后释放锁定可能是件好事.否则,如果100个线程各自尝试同时更新,则在最坏的情况下,它们可能必须平均每个进行大约50次复制更新 - 尝试交换操作(总共5,000次复制 - 更新 - 尝试 - 交换操作以执行100次更新). (23认同)
  • @ jnm2:这些集合使用了CompareExchange很多,但我不认为它们会进行大量的复制.CAS +锁不一定是多余的,如果使用它来允许并非所有编写者都获得锁定的可能性.例如,争用通常很少但偶尔可能很严重的资源可能有一个锁和一个标志,指示编写者是否应该获取它.如果标志清楚,作者只需做一个CAS,如果成功,他们继续前进.然而,一个使CAS失败超过两次的作者可以设置标志; 然后国旗可以保持设定...... (3认同)

Bri*_*eon 21

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

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

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

  • 不再真实了。在现代硬件上,即使具有相同的读取器和写入器(4 个读取器和 4 个写入器),ReaderWriterLockSlim 也比 lock(这是 Monitor 的快捷方式)快大约 9%。使用 BenchmarkDotNet 对其进行基准测试并查看。 (2认同)

Han*_*ant 15

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

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


Dan*_*Tao 10

编辑2:只需删除Thread.Sleep来自ReadThread和的电话WriteThread,我看到Locked表现优异RWLocked.我相信汉斯在这里击中了钉子; 你的方法太快,不会产生争用.当我添加Thread.Sleep(1)Get和的Add方法LockedRWLocked(并使用4个读取线程对抗1个写线程)时,RWLocked打败了裤子Locked.


编辑:好的,如果我真的在我第一次发布这个答案的时候,我至少会意识到你为什么要把这些Thread.Sleep调用放在那里:你试图重新发现读取的场景比写入更频繁.这不是正确的方法.相反,我会引入额外的开销,你AddGet方法来创建争的机会较大(如汉斯建议),创造更多的读取线程,比写线程(以确保更频繁的读比写),并删除Thread.Sleep从通话ReadThreadWriteThread(事实上,减少争用,实现与你想要的相反).


我喜欢你到目前为止所做的一切.但是这里有一些我看到的问题:

  1. 为什么Thread.Sleep打电话?这些只会使您的执行时间膨胀一个恒定的数量,这将人为地使性能结果收敛.
  2. 我也不会Thread在你的测量代码中包含新对象的创建Stopwatch.这不是一个创造的微不足道的对象.

一旦你解决了上述两个问题,你是否会看到显着的差异,我不知道.但我相信在讨论继续之前应该解决这些问题.


小智 8

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