Когда 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
кстати, если я уберу спящие: «Заблокировано: 89 мс. RW Заблокировано: 32 мс.» - или увеличивая числа: «Заблокировано: 1871 мс. RW Заблокировано: 2506 мс.».
Марк Гравелл
1
Если я уберу
спящий режим
1
Это потому, что код, который вы синхронизируете, слишком быстр, чтобы создавать какие-либо конфликты. См. Ответ Ганса и мое редактирование.
Дэн Тао

Ответы:

107

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

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

Марк Гравелл
источник
1
Копирование и замена при записи - определенно лучший вариант.
LBushkin
24
Неблокирующее копирование и замена при записи полностью исключает конфликт ресурсов или блокировку для читателей; это быстро для писателей, когда нет разногласий, но может сильно застревать в журналах при наличии разногласий. Было бы хорошо, если бы писатели установили блокировку перед созданием копии (читатели не заботились бы о блокировке) и сняли блокировку после выполнения обмена. В противном случае, если каждый из 100 потоков попытается выполнить одно одновременное обновление, им в худшем случае придется выполнить в среднем около 50 операций копирования-обновления-попытки-замены каждая (всего 5000 операций копирования-обновления-попытки-замены для выполнения 100 обновлений).
supercat
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
1
@mcmillab в основном да; в некоторых простых случаях (обычно при обертывании одного значения или когда не имеет значения, если позже будет перезаписано то, что они никогда не видели), вы даже можете избежать потери Interlockedи просто использовать volatileполе и просто назначить ему - но если вам нужно быть уверенным, что ваша сдача не потерялась полностью, то Interlockedэто идеальный
вариант
3
@ jnm2: Коллекции часто используют CompareExchange, но я не думаю, что они много копируют. Блокировка CAS + не обязательно является избыточной, если ее использовать, чтобы учесть возможность того, что не все писатели получат блокировку. Например, ресурс, за который конкуренция обычно бывает редко, но иногда может быть серьезной, может иметь блокировку и флаг, указывающий, должны ли авторы получать его. Если флажок снят, писатели просто выполняют CAS, и, если это удается, они продолжают свой путь. Однако писатель, который терпит неудачу CAS более двух раз, может установить флаг; тогда флаг может оставаться установленным ...
supercat 08
23

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

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

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

Брайан Гидеон
источник
17

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

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

Ганс Пассан
источник
12

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


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


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

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

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

Дэн Тао
источник
+1: Хороший момент по пункту 2 - он действительно может исказить результаты (разница между временами одинакова, но другая пропорция). Хотя, если вы запустите это достаточно долго, время создания Threadобъектов станет незначительным.
Нельсон Ротермел
10

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

i255
источник
Мне это нравится. Статья заставляет меня задуматься, работает ли Monitor быстрее, чем ReaderWriterLockTiny на каждой платформе?
jnm2 08
Хотя ReaderWriterLockTiny быстрее, чем ReaderWriterLockSlim (а также Monitor), есть недостаток: если есть много читателей, которые занимают довольно много времени, он никогда не давал писателям шанса (они будут ждать почти бесконечно) .ReaderWriterLockSlim, с другой стороны, удастся сбалансировать доступ к общим ресурсам для читателей / писателей.
tigrou
3

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

MSN
источник
3

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

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

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

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

Стив Таунсенд
источник
2

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

Итай Каро
источник
0

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

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

Иллидан
источник