Когда ReaderWriterLockSlim лучше простой блокировки?

Я делаю очень глупый тест для 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 раз чаще, чем пишет, производительность остается (почти) такой же.

Я что-то здесь не так делаю?

С уважением.

11 ответов

Решение

В вашем примере сны означают, что обычно нет споров. Необслуживаемая блокировка очень быстрая. Для того чтобы это имело значение, вам понадобится состязательный замок; если в этом утверждении есть записи, они должны быть примерно одинаковыми (lock может даже быть быстрее) - но если это в основном читает (с конфликтом записи редко), я бы ожидал ReaderWriterLockSlim заблокировать, чтобы превзойти lock,

Лично я предпочитаю другую стратегию здесь, использующую обмен ссылками - поэтому чтения всегда можно прочитать без проверки / блокировки / и т. Д. Записи вносят свои изменения в клонированную копию, а затем используют Interlocked.CompareExchange поменять ссылку (повторно применить их изменение, если другой поток мутировал ссылку в промежуточный период).

Мои собственные тесты показывают, что ReaderWriterLockSlim имеет в 5 раз больше накладных расходов по сравнению с обычным lock, Это означает, что для RWLS, чтобы превзойти обычную старую блокировку, следующие условия обычно имели бы место.

  • Количество читателей значительно превышает количество писателей.
  • Замок должен был удерживаться достаточно долго, чтобы преодолеть дополнительные накладные расходы.

В большинстве реальных приложений этих двух условий недостаточно, чтобы преодолеть эту дополнительную нагрузку. В частности, в вашем коде блокировки удерживаются в течение такого короткого периода времени, что, вероятно, доминирующим фактором будут издержки блокировки. Если бы вы переместить эти Thread.Sleep звонки внутри замка, тогда вы, вероятно, получите другой результат.

Там нет споров в этой программе. Методы Get и Add выполняются за несколько наносекунд. Вероятность попадания нескольких потоков в эти методы в одно и то же время крайне мала.

Поместите в них вызов Thread.Sleep(1) и удалите сон из потоков, чтобы увидеть разницу.

Редактировать 2: просто удалив Thread.Sleep звонки из ReadThread а также WriteThread, Я видел Locked опережать RWLocked, Я полагаю, что Ганс ударил гвоздь по голове здесь; ваши методы слишком быстрые и не вызывают споров. Когда я добавил Thread.Sleep(1) к Get а также Add методы Locked а также RWLocked (и использовал 4 потока чтения против 1 потока записи), RWLocked сбить штаны Locked,


Изменить: ОК, если бы я действительно думал, когда я впервые опубликовал этот ответ, я бы по крайней мере понял, почему вы поставили Thread.Sleep звонки туда: вы пытались воспроизвести сценарий чтения, который происходит чаще, чем запись. Это просто неправильный способ сделать это. Вместо этого я бы добавил дополнительные накладные расходы на ваш Add а также Get методы, позволяющие повысить вероятность конфликта (как предложил Ханс), создать больше потоков чтения, чем потоков записи (чтобы обеспечить более частое чтение, чем запись), и удалить Thread.Sleep звонки из ReadThread а также WriteThread (которые фактически уменьшают раздоры, достигая противоположности того, что вы хотите).


Мне нравится то, что вы сделали до сих пор. Но вот несколько вопросов, которые я вижу сразу:

  1. Почему Thread.Sleep звонки? Это просто увеличивает ваше время выполнения на постоянную величину, что искусственно приводит к сближению результатов производительности.
  2. Я также не буду включать создание новых Thread объекты в коде, который измеряется вашим Stopwatch, Это не тривиальный объект для создания.

Увидите ли вы значительную разницу после того, как решите две вышеуказанные проблемы, я не знаю. Но я считаю, что они должны быть рассмотрены до того, как обсуждение продолжится.

Вы получите лучшую производительность с ReaderWriterLockSlim чем простая блокировка, если вы блокируете часть кода, для выполнения которой требуется больше времени. В этом случае читатели могут работать параллельно. Приобретение ReaderWriterLockSlim занимает больше времени, чем ввод простого Monitor, Проверь мой ReaderWriterLockTiny реализация для блокировки чтения-записи, которая даже быстрее простого оператора блокировки и предлагает функциональность чтения-записи: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/

Неоспоримые блокировки занимают порядка микросекунд для получения, поэтому время выполнения будет меньше ваших звонков Sleep,

Если у вас нет многоядерного оборудования (или, по крайней мере, того же, что и запланированная производственная среда), вы не получите реалистичный тест здесь.

Более разумным тестом было бы продлить срок службы ваших заблокированных операций, поместив небольшую задержку внутри блокировки. Таким образом, вы действительно сможете противопоставить параллелизм, добавленный с помощью ReaderWriterLockSlim по сравнению с сериализацией, подразумеваемой основным lock(),

В настоящее время время, затрачиваемое вашими заблокированными операциями, теряется из-за шума, генерируемого вызовами Sleep, которые происходят вне блокировок. Общее время в любом случае в основном связано со сном.

Вы уверены, что ваше реальное приложение будет иметь одинаковое количество операций чтения и записи? ReaderWriterLockSlim действительно лучше для случая, когда у вас много читателей и относительно редких писателей. 1 поток писателя против 3 потоков читателя должен продемонстрировать ReaderWriterLockSlim преимущества лучше, но в любом случае ваш тест должен соответствовать ожидаемому реальному шаблону доступа.

Проверьте эту статью: http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx

Ваши сны, вероятно, достаточно длинные, чтобы сделать вашу блокировку / разблокировку статистически незначимой.

Я предполагаю, что это из-за снов, которые вы имеете в своих нитях читателя и писателя.
Ваша ветка чтения спит 500tims 50ms, что составляет 25000 большую часть времени, когда она спит

Опция копирования и сравнения-обмена возможна только в том случае, когда спорный ресурс представляет собой одну ячейку памяти (например, int).

Когда операция чтения может включать в себя запись (например, поиск элемента в дереве и вставка, если он не найден), вполне вероятно, что копирования с последующим сравнением и обменом будет недостаточно из-за условий гонки при обновлении нескольких элементов. места в памяти — подумайте об очереди «голова-хвост»).

Единственными жизнеспособными вариантами (по моему опыту) в этой ситуации являются:

[1] Используйте спин-блокировки в (многоядерной среде), когда типичная транзакция «читается» и очень короткая (например, поиск по дереву, бинарный поиск,...)

[2] Используйте ReaderWriterLockSlim и обновите во время транзакции, если обновление необходимо.

Когда ReaderWriterLockSlim лучше простой блокировки?

Когда у вас значительно больше чтения, чем записи.

Другие вопросы по тегам