320x100
728x90

 

📌  ReaderWriterLock 이란? 

           읽기 전용 작업(Read-Only) 에 대해서는 동시 접근을 허용하고, 쓰기 작업에 대해서는 독점 접근(exclusive)을 함.  

 

 

          🤔 왜, 언제 사용할까 ❗❓

 

                 lock을 사용하면 오직 하나의 스레드만 코드(임계영역)에 접근할 수 있다. 

                 하지만 하나의 스레드만이 쓰고 읽을 수 있다면 매우 시간도 오래걸리고 비효율적일 것이다.

 

         👉 ReaderWriterLock은 이를 해결해준다. 다수의 스레드들이 읽을 수 있게 하며, 쓰기 전용 권한을 가질 수 있도록 한다.                 다수의 독자가 있고 작성자가 적은 경우에 효율적이다. 

 

 

              Read :  다수의 스레드가 접근 가능

                          다른 쓰레드가 쓰고(Write)있으면 대기 -> Write Lock이 해제 되어야 진입 가능

 

              Write :  동시에 하나의 스레드만 수정(접근) 가능

                           읽고 있는 중에 데이터가 수정되면 안되므로, 모든 Read Lock이 해제되어 있는 상황이어야 함

                                   -> ReadLock이 해제될 때 까지 대기

 

 

 

 

 

💥  ReaderWriterLock 구현하기 

 

        👉  재귀적 Lock을 허용하지 않는 상황 

                 설정 :  스핀락 사용, 최대 스핀 횟수(MAX_SPIN_COUNT) 는 오천번으로 설정

                            32비트를 사용한다. 

 

 

 

                 찬찬히 코드를 살펴보자 

 

 


 

 

             1️⃣  변수 및 플래그 설정 

 

                    EMPTY_FLAG 는 플래그 기본 값,

                    WRITE_MASK는 1부터 15번째의 비트(15개),

                    READ_MASK 는 16부터 31번째 비트를 사용한다(16개)

 

                    MAX_SPIN_LOCK은 스핀락 최대 시도 횟수로 5천번으로 제한해 두었다. 

                    _flag 값은 초기 기본 플래그 값인 EMPTY_FLAG를 할당한다. 

 

 

            2️⃣  함수 만들기 

                   

                  진입 시도하여 락을 걸어주는 WriteLock()과 

                  진입 했을 때, 락을 풀어주는 WriteUnLock() 

                  그리고 마찬가지로 ReadLock()ReadUnLock()을 껍데기만 일단 만들어 두자.

 

 

    // 재귀적 락을 허용할 지(No)
    // 스핀락 정책 (5000번 -> Yield)
class Lock
{
    const int EMPTY_FLAG = 0x00000000;
    const int WRITE_MASK = 0x7FFF0000;
    const int READ_MASK = 0x0000FFFF;
    const int MAX_SPIN_COUNT = 5000;

    // [ Unused(1)] [WriteThreadId(15)] -> 한번에 한 스레드만 획득 가능 [ReadCount(16)]
    int _flag = EMPTY_FLAG;

    public void WriteLock()
    {
        ...
    }

    public void WriteUnlock()
    {
         ...
    }

    public void ReadLock()
    {
         ...
    }

    public void ReadUnlock()
    {
         ...
    }

}

 

 

              3️⃣  WriteLock 과 WriteUnLock 구현하기

 

                  👉 WriteLock 

                     1) Thread.CurrentThread.ManagedThreadId를 통해 현재 스레드 Id를 얻고,

                         WRITE_MASK와의 논리 연산을 통해 desire 값으로 초기화 한다. 

 

                     2) _flag의 값을 확인하면서 lock을 획득하고 있는 지 아닌지를 반복문을 돌면서 확인한다.

 

                     3) _flag 값이 EMPTY_FLAG와 동일하면 _flag값에 desire 값을 할당한다

 

                        (진입하여 lock을 획득한 다음, 반복문을 나온다. ❗ 이 때,  Interlocked의 compareExchange를 활용해 값을

                         비교하고 초기화 하는 작업이 한 번에 이루어 지도록 한다. )

 

                     4)  5천번 이상 시도를 했을 때는, Yield()를 통해 다른 스레드에게 점유를 넘긴다. 

 

 

                👉 WriteUnLock 

                      1) 쓰기가 종료되면 락을 풀어 주어야 한다.

                         _flag 값을 다시 초기값(EMPTY_FLAG == 0) 으로 바꾼다. 

 

 

public void WriteLock()
{
    // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다
    int desire = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
    while (true)
    {
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            if (Interlocked.CompareExchange(ref _flag, desire, EMPTY_FLAG) == EMPTY_FLAG)
            {
                return;
            }

            Thread.Yield();
        }
    }
}

public void WriteUnlock()
{
    Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}

 

 

 

 

              4️⃣ ReadLock과 ReadUnlock 구현하기

 

                    1️⃣ Readlock

                    1) _flag와 READ_MASK 논리 연산 값을 expected에 할당한다. 

                       (읽을 수 있는 지 여부를 확인하기 위한 변수 설정)

 

                    2) _flag와 expected 값이 같으면, expected 값을 1 증가 (write 상태가 아님을 의미)

 

                    3) 최대 시도 횟수를 넘어가면 다른 스레드에게 Yield()를 통해 양보한다.

 

 

                    👉 ReadUnlock

                    1) 락을 풀어주기 위해 증가시켰던 값을 Interlocked 통해 감소시킨다 

 

public void ReadLock()
{
    while (true)
    {
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            int expected = (_flag & READ_MASK);
            if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                return;

            //해당 방법은 스레드 동기화가 잘 이루어지지 않는 문제 발생 -> 한번에 처리하는 코드로 바꿔야 함 
            /*if((_flag & WRITE_MASK) == 0)
            {
                _flag = _flag + 1;
                return;
            }*/
        }

        Thread.Yield();
    }
}

public void ReadUnlock()
{
    Interlocked.Decrement(ref _flag); // 1을 줄임 
}

 

 

 

 

    👉   재귀적 락을 허용하는 경우 

 

             재귀적 락을 허용하는 경우란 다음 경우를 말한다. 

             동일 스레드 내에서(Write 스레드가) 하고 Write 하는 경우 Ok

             Write -> Read 하는 경우도 Ok

 

            하지만 동일 스레드 내에서 Read -> Write하는 것은 막는다.

            즉, 읽으면서 쓰는 경우는 막는 것이다.  

 

 

            1️⃣   WriteLock과 WriteUnlock의 달라지는 점

 

                      👉 Writelock

                    1) 현재 스레드 ID와 lock을 소유하고 있는 ID를 확인하여 이미 writelock 을 획득하고 있는 지 확인한다.

                        1-2) 획득하고 있으면, _writeCount 변수를 두어 값을 증가시킨다. (재귀적 허용)

                                소유하고 있지 않을 때는 기존과 같다.

 

                    👉 WriteUnlock

                     1) lock을 획득한 만큼(증가시킨 만큼) 풀어주어야 한다. _writeCount 의 값을 감소시킨다.

                     2) _writeCount가 0이 되었을 때, write을 unlock 시킨다. (초기 상태로 세팅한다.) 

 

 

            2️⃣   ReadLock과 ReadUnlock의 달라지는 점

 

                      1) 현재 스레드가 이미 writeLock을 획득하고 있는 지 확인한다.

                      2) 소유하고 있다면  _flag 값 자체를 증가시킨다. (_flag 값 자체가 ReadCount가 된다 - 공유 카운트) 

 

                     소유하고 있지 않을 때는 동일하며 ReadUnlock 또한 동일하다. 

 

 

최종 코드 

// 재귀적 락을 허용할 지(No)
// 재귀적 락을 허용할 지(YES) WriteLock -> WriteLock OK, WriteLock->ReadLock Ok, ReadLock -> WriteLock No
// 스핀락 정책 (5000번 -> Yield)
class Lock
{
const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000;
const int READ_MASK = 0x0000FFFF;
const int MAX_SPIN_COUNT = 5000;

// [ Unused(1)] [WriteThreadId(15)] -> 한번에 한 스레드만 획득 가능 [ReadCount(16)]
int _flag = EMPTY_FLAG;
int _writeCount = 0;


public void WriteLock()
{
    //동일 스레드가 WriteLock을 이미 획득하고 있는 지 확인
    int lockThreadId = (_flag & WRITE_MASK) >> 16;
    if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
    {
        _writeCount++;
        return;
    }
    //아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다
    int desire = (Thread.CurrentThread.ManagedThreadId << 16) && WRITE_MASK;
    while (true)
    {
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            if (Interlocked.CompareExchange(ref _flag, desire, EMPTY_FLAG) == EMPTY_FLAG)
            {
                _writeCount = 1;
                return;
            }


            /*//시도를 해서 성공하면 return 
            if (_flag == EMPTY_FLAG)
                _flag = desire;*/
        }

        Thread.Yield();

    }
}

public void WriteUnlock()
{
    int lockCount = --_writeCount;
    if (lockCount == 0)
        //초기 상태로 돌려줌
        Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}

public void ReadLock()
{
    //동일 스레드가 WriteLock을 이미 획득하고 있는 지 확인
    int lockThreadId = (_flag & WRITE_MASK) >> 16;
    if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
    {
        Interlocked.Increment(ref _flag);
        return;
    }

    //아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1 늘린다. 
    while (true)
    {
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            int expected = (_flag & READ_MASK);
            if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                return;
            //해당 방법은 스레드 동기화가 잘 이루어지지 않는 문제 발생 -> 한번에 처리하는 코드로 바꿔야 함 
            /*if((_flag & WRITE_MASK) == 0)
            {
                _flag = _flag + 1;
                return;
            }*/
        }

        Thread.Yield();
    }
}

public void ReadUnlock()
{
    Interlocked.Decrement(ref _flag); // 1을 줄임 
}

}

 

 

메인 실행문 

static volatile int count = 0;
static Lock _lock = new Lock();

static void Main(string[] args)
{
    Task t1 = new Task(delegate ()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.WriteLock();
            count++;
            _lock.WriteUnlock();
        }
    });

    Task t2 = new Task(delegate ()
    {
        for(int i=0; i < 100000; i++)
        {
            _lock.ReadLock();
            count--;
            _lock.ReadUnlock();
        }
    });

    t1.Start();
    t2.Start();

    Task.WaitAll(t1, t2);

    Console.WriteLine(count);

}

 

 

 


 

 

 ⭐ 정리(재귀적 허용일 때)

          WriteLock 메소드 :   스레드 Id를 확인하여 write 락을 소유하고 있는 지 확인한다. 

                                           다른 스레드는 현재 스레드가 소유권을 포기하기 전까지 대기 상태로 있어야 한다.

                                          (소유권은 한 스레드만 획득 가능)

                                         동일한 스레드가 소유권을 또 가지려고 할 때, 별도의 _writeCount 변수를 두어 이 값을 증가시킨다. 

 

          WriteUnlock 메소드 :  _writeCount 가 증가된 만큼 감소시키고 그 값이 0이 되면 락을 해제한다. 

 

          ReadLock 메소드 :  write을 소유하고 있지 않을 때, 다수의 스레드가 경합하여 공유 카운트를 올리게 한다.

                                         동일한 스레드가 소유하고 있을 때에도 공유 카운트를 증가시켜야 한다. 

 

          ReadUnlock 메소드 :  공유 카운트의 값을 감소시켜 락을 풀어준다. 

 

728x90
반응형