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
btw, 만약 내가 수면을 취하면 : "잠김 : 89ms. RW 잠김 : 32ms."-또는 숫자를 높이는 : "잠김 : 1871ms. RW 잠김 : 2506ms.".
Marc Gravell

1
수면을 제거하면 잠김이 rwlocked보다 빠릅니다
vtortola

1
동기화하는 코드가 너무 빨라 경합을 일으키지 않기 때문입니다. Hans의 답변과 내 편집을 참조하십시오.
Dan Tao

답변:


107

귀하의 예에서 수면은 일반적으로 경합이 없음을 의미합니다 . 비경쟁 잠금은 매우 빠릅니다. 이 문제를 해결하려면 경합 잠금 이 필요합니다 . 이 경우 쓰기가 그 경합, 그들에 대해해야 같은 ( lock보다 빠르고있을 수 있습니다) - 그러나이 경우 대부분 (거의 쓰기 경합으로) 읽고, 나는 기대 ReaderWriterLockSlim을 수행 아웃에 잠금 장치를 lock.

개인적으로, 여기서는 참조 교환을 사용하는 다른 전략을 선호합니다. 따라서 읽기는 항상 확인 / 잠금 등없이 읽을 수 있습니다. 쓰기는 복제 된 복사본을 변경 한 다음 Interlocked.CompareExchange참조를 교환하는 데 사용 합니다 (다른 스레드가있는 경우 변경 사항을 다시 적용) 중간에 참조를 변경했습니다).


1
Copy-and-swap-on-write는 확실히 여기로가는 길입니다.
LBushkin 2010

24
비 잠금 copy-and-swap-on-write는 리더에 대한 리소스 경합이나 잠금을 완전히 방지합니다. 경합이없는 경우 작성자에게는 빠르지 만 경합이있는 경우 심하게 방해가 될 수 있습니다. 사본을 만들기 전에 작성자가 잠금을 획득하고 (독자는 잠금에 대해 관심이 없음) 스왑을 수행 한 후 잠금을 해제하는 것이 좋습니다. 그렇지 않으면 100 개의 스레드가 각각 한 번의 동시 업데이트를 시도하는 경우 최악의 경우 각각 평균 ​​약 50 개의 copy-update-try-swap 작업을 수행해야 할 수 있습니다 (100 개의 업데이트를 수행하기 위해 총 5,000 개의 copy-update-try-swap 작업).
supercat 2010

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에 이상적입니다
마크 Gravell

3
@ jnm2 : 컬렉션은 CompareExchange를 많이 사용하지만 복사 작업을 많이하는 것 같지는 않습니다. CAS + lock은 모든 작성자가 잠금을 획득하지 못할 가능성을 허용하기 위해 사용하는 경우 반드시 중복되는 것은 아닙니다. 예를 들어, 경합이 일반적으로 드물지만 때때로 심각 할 수있는 리소스에는 작성자가 획득해야하는지 여부를 나타내는 잠금 및 플래그가있을 수 있습니다. 플래그가 명확하면 작성자는 단순히 CAS를 수행하고 성공하면 계속 진행합니다. 그러나 CAS에 두 번 이상 실패한 기록기는 플래그를 설정할 수 있습니다. 플래그는 ... 설정을 유지 할 수
supercat

23

내 자신의 테스트에 따르면 ReaderWriterLockSlim정상에 비해 약 5 배의 오버 헤드가 lock있습니다. 즉, RWLS가 일반 이전 잠금을 능가하려면 일반적으로 다음 조건이 발생합니다.

  • 독자의 수는 작가보다 훨씬 많습니다.
  • 추가 오버 헤드를 극복 할 수 있도록 잠금을 충분히 오래 유지해야합니다.

대부분의 실제 애플리케이션에서이 두 가지 조건은 추가 오버 헤드를 극복하기에 충분하지 않습니다. 특히 코드에서 잠금은 잠금 오버 헤드가 지배적 인 요소가 될 수있는 짧은 시간 동안 유지됩니다. 이러한 Thread.Sleep호출을 잠금 내부 로 이동 하면 다른 결과를 얻을 수 있습니다.


17

이 프로그램에는 경합이 없습니다. Get 및 Add 메서드는 몇 나노초 안에 실행됩니다. 여러 스레드가 정확한 시간에 이러한 메서드를 사용할 확률은 매우 적습니다.

Thread.Sleep (1) 호출을 넣고 스레드에서 수면을 제거하여 차이점을 확인하십시오.


12

편집 2 : 단순히 및 에서 Thread.Sleep호출을 제거하면 성능 이 뛰어납니다 . 나는 Hans 가 여기 머리에 못을 박았 다고 믿습니다 . 당신의 방법은 너무 빠르며 경합을 일으키지 않습니다. 내가 추가 할 때 받는 및 방법 과 , (그리고 사용 4 1 개 쓰기 스레드에 대한 스레드를 읽기) 떨어져 바지를 이길 .ReadThreadWriteThreadLockedRWLockedThread.Sleep(1)GetAddLockedRWLockedRWLockedLocked


편집은 : 나는 실제로 있다면 OK, 생각 내가 처음이 답변을 게시 할 때, 나는 적어도 실현 한 것입니다 당신은 넣어 Thread.Sleep거기에 전화를 : 당신은 쓰기보다 더 자주 일어나는 읽기의 시나리오를 재현하고있었습니다. 이것은 올바른 방법이 아닙니다. 대신에, 나는 당신에게 여분의 오버 헤드를 소개하는 것 Add하고 Get(같은 경쟁의 더 큰 기회를 만드는 방법 한스 제안 (쓰기보다 읽기 더 자주 확인하기 위해) 쓰기 스레드보다 더 많은 읽기 스레드를 생성하고 제거) Thread.Sleep에서 전화 ReadThreadWriteThread(실제로하는 경쟁을 줄이고 원하는 것과 반대).


나는 당신이 지금까지 한 것을 좋아합니다. 하지만 바로 다음과 같은 몇 가지 문제가 있습니다.

  1. Thread.Sleep전화합니까? 이는 실행 시간을 일정한 양만큼 부풀려서 인위적으로 성능 결과를 수렴하게 만듭니다.
  2. 또한 .NET Core로 Thread측정되는 코드 에 새 개체 생성을 포함하지 않을 것 Stopwatch입니다. 그것은 만드는 사소한 개체가 아닙니다.

위의 두 가지 문제를 해결하면 큰 차이가 있는지 여부는 모르겠습니다. 그러나 나는 토론이 계속되기 전에 그것들이 해결되어야한다고 믿는다.


+1 : # 2의 좋은 점은 실제로 결과를 왜곡 할 수 있습니다 (각 시간간에 동일한 차이, 그러나 다른 비율). 이렇게 오래 실행하면 Thread개체를 만드는 시간 이 줄어들 기 시작합니다.
Nelson Rothermel

10

ReaderWriterLockSlim실행 시간이 더 오래 걸리는 코드의 일부를 잠그면 단순 잠금보다 더 나은 성능을 얻을 수 있습니다. 이 경우 독자는 병렬로 작업 할 수 있습니다. 을 획득하는 ReaderWriterLockSlim것은 간단한 Monitor. ReaderWriterLockTiny간단한 잠금 문보다 훨씬 빠르고 독자-작성기 기능을 제공하는 독자-작성기 잠금에 대한 구현을 확인하십시오 . http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/


나는 이것을 좋아한다. 이 기사는 모니터가 각 플랫폼에서 ReaderWriterLockTiny보다 빠른지 궁금해합니다.
jnm2 2015

ReaderWriterLockTiny는 ReaderWriterLockSlim (및 Monitor)보다 빠르지 만 단점이 있습니다. 꽤 많은 시간이 걸리는 독자가 많으면 작성자에게 기회를주지 않습니다 (거의 무한정 기다릴 것입니다). 독자 / 작가를위한 공유 리소스에 대한 균형 액세스.
tigrou


3

경합되지 않은 잠금은 획득하는 데 마이크로 초 정도가 걸리므 로을 호출하면 실행 시간이 작아집니다 Sleep.


3

멀티 코어 하드웨어 (또는 최소한 계획된 프로덕션 환경과 동일)가 없다면 여기서 현실적인 테스트를받을 수 없습니다.

보다 현명한 테스트는 잠금 내부에 잠시 지연을 두어 잠긴 작업의 수명을 연장하는 것 입니다. 이렇게하면 ReaderWriterLockSlim기본에서 암시하는 직렬화와 사용하여 추가 된 병렬 처리를 실제로 대조 할 수 있어야합니다 lock().

현재 잠긴 작업에 걸리는 시간은 잠금 외부에서 발생하는 Sleep 호출에 의해 생성 된 소음으로 인해 손실됩니다. 두 경우 모두 총 시간은 대부분 수면과 관련이 있습니다.

실제 앱의 읽기 및 쓰기 횟수가 동일 할 것이라고 확신합니까? ReaderWriterLockSlim독자가 많고 작가가 비교적 드문 경우에 정말 좋습니다. 1 개의 작성기 스레드와 3 개의 판독기 스레드가 ReaderWriterLockSlim더 나은 이점 을 보여 주어야 하지만 어떤 경우에도 테스트는 예상되는 실제 액세스 패턴과 일치해야합니다.


2

나는 이것이 당신이 독자와 작가 스레드에서 잠들었 기 때문이라고 생각합니다.
읽기 스레드는 500tims 50ms 절전 모드로 25000입니다. 대부분의 경우 절전 모드입니다.


0

ReaderWriterLockSlim이 단순 잠금보다 나은 경우는 언제입니까?

쓰기보다 읽기가 훨씬 더 많을 때.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.