Коли ReaderWriterLockSlim кращий за простий замок?


80

Я роблю дуже безглуздий орієнтир на ReaderWriterLock з цим кодом, де читання відбувається в чотири рази частіше, ніж написання:

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
Це тому, що код, який ви синхронізуєте, занадто швидкий, щоб створити будь-які суперечки. Див. Відповідь Ганса та мою редакцію.
Dan Tao

Відповіді:


107

У вашому прикладі сплячі означають, що взагалі немає суперечок. Безперервний замок дуже швидкий. Щоб це мало значення, вам знадобився б заперечуваний замок; якщо в цій суперечці є записи , вони повинні бути приблизно однаковими ( lockможе бути навіть швидшими) - але якщо це в основному читається (із суперечкою про запис рідко), я би очікував, що ReaderWriterLockSlimблокування перевиконаєlock .

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


1
Копіювати і міняти місцями на запис - це, безумовно, шлях сюди.
Л.Бушкін

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

23

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

  • Кількість читачів значно перевершує письменників.
  • Замок потрібно було тримати досить довго, щоб подолати додаткові накладні витрати.

У більшості реальних додатків цих двох умов недостатньо, щоб подолати ці додаткові накладні витрати. Зокрема, у вашому коді замки утримуються настільки короткий проміжок часу, що накладні витрати на замок, ймовірно, будуть головним фактором. Якби ви перенесли ці Thread.Sleepдзвінки всередину блокування, то, ймовірно, ви отримали б інший результат.


17

У цій програмі немає суперечок. Методи Get і Add виконуються за кілька наносекунд. Шанси на те, що декілька потоків потрапляють на ці методи в один і той же час, зникають малі.

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


12

Редагування 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. Це не тривіальний об’єкт для створення.

Чи ви побачите суттєву різницю, коли вирішите ці два питання, я не знаю. Але я вважаю, що їх слід вирішити до продовження обговорення.


+1: Хороший момент на №2 - він може по-справжньому спотворити результати (однакова різниця між часом, але інша пропорція). Хоча якщо запустити це досить довго, час створення Threadоб’єктів почав би ставати незначним.
Нельсон Ротермель

10

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


Мені подобається це. Стаття змушує мене замислюватися, чи монітор швидший за ReaderWriterLockTiny на кожній платформі?
jnm2

Хоча ReaderWriterLockTiny швидше, ніж ReaderWriterLockSlim (а також Monitor), є недолік: якщо багато читачів займає досить багато часу, це ніколи не дало шансу письменникам (вони чекатимуть майже нескінченно довго). ReaderWriterLockSlim, з іншого боку, вдасться збалансувати доступ до спільного ресурсу для читачів / письменників.
tigrou


3

Непереборні замки займають порядок мікросекунд, тому ваш час виконання буде занижений вашими дзвінками Sleep.


3

Якщо у вас немає багатоядерного обладнання (або, принаймні, такого ж, як і заплановане виробниче середовище), ви не отримаєте тут реалістичного тесту.

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

В даний час час, зайнятий вашими операціями, які заблоковані, втрачається під час шуму, викликаного викликами сплячого режиму, що відбуваються поза блокуваннями. Загальний час в обох випадках переважно пов'язаний зі сном.

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


2

Думаю, це пов’язано зі сном у вас у читацьких та письменницьких нитках.
У вашій прочитаній ланцюжку сплять 500 тимів 50 мс, що становить 25000. Більшу частину часу він спить


0

Коли ReaderWriterLockSlim кращий за простий замок?

Коли у вас значно більше читає, ніж пише.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.