본문 바로가기
공부/게임서버

재귀적 lock을 허용하지 않는 ReaderWriterLock 구현 예시

by samosa 2024. 11. 11.
namespace ServerCore
{ 
    // 구현 정책
    // 1. 재귀적 락은 허용하지 않는다.
    // 2. 스핀락은 5000번 시도한 후에 Yield로 양보한다.

    public class ReaderWriterLock
    {
        private const int EMPTY_FLAG = 0x00000000;
        private const int WRITE_MASK = 0x7FFF0000;
        private const int READ_MASK = 0x0000FFFF;
        private const int MAX_SPIN_COUNT = 5000;

        // 32 bits : [Unused(1)] [WriteThread(15)] [ReadCount(16)]

        private int _flag = EMPTY_FLAG;

        public void WriteLock()
        {
            int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;

            while (true)
            {
                for (var i = 0; i < MAX_SPIN_COUNT; i++)
                    // 아무도 사용하고 있지 않다면
                    if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                        return;

                Thread.Yield();
            }
        }

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

        public void ReadLock()
        {
            while (true)
            {
                // 아무도 WriteLock을 사용하고 있지 않다면, ReadLock을 1 증가시킨다.
                for (var i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    int expected = (_flag & READ_MASK); // write lock을 다 무시하고 read lock만 확인
                    if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                        return;
                }
            }
        }

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}

 

이 코드는 ServerCore 네임스페이스 내의 ReaderWriterLock 클래스이다. 이 클래스는 읽기-쓰기 잠금을 구현하여 여러 스레드가 안전하게 데이터에 접근할 수 있도록 보장한다. 구현 정책은 재귀적 잠금을 허용하지 않으며, 스핀락을 최대 5000번 시도한 후 잠금을 얻지 못하면 스레드에게 실행 순서를 양보한다는 것이다.

주요 상수와 변수 설명

  • EMPTY_FLAG: 초기 상태를 나타내며, 아무도 잠금을 사용하지 않을 때 0x00000000의 값을 갖는다.
  • WRITE_MASK: 쓰기 잠금을 위한 비트 마스크로, 0x7FFF0000 값을 갖는다. 상위 15비트를 사용하여 쓰기 스레드를 식별한다.
  • READ_MASK: 읽기 잠금만을 확인하기 위한 마스크로, 하위 16비트를 나타낸다.
  • MAX_SPIN_COUNT: 스핀락을 시도하는 최대 횟수로, 이 횟수에 도달하면 스레드가 Thread.Yield()로 실행을 양보한다.
  • _flag: 현재 잠금 상태를 나타내는 32비트 변수이다. 비트 구성은 [Unused(1)] [WriteThread(15)] [ReadCount(16)]로 되어 있다.

메소드 설명

  1. WriteLock 메소드
    • 현재 스레드 ID를 사용해 쓰기 잠금을 시도한다. 스레드 ID를 16비트 왼쪽으로 이동해 _flag의 상위 비트에 설정하고, 이를 WRITE_MASK와 결합해 desired 값을 만든다.
    • MAX_SPIN_COUNT만큼 _flagEMPTY_FLAG인지 확인하며 반복 시도한다. _flag가 비어 있다면 Interlocked.CompareExchange를 사용해 _flagdesired 값으로 변경하여 잠금을 얻는다.
    • 잠금을 얻지 못한 경우, Thread.Yield()를 호출해 다른 스레드에게 실행을 양보한다.
  2. WriteUnlock 메소드
    • Interlocked.Exchange를 통해 _flag 값을 EMPTY_FLAG로 초기화하여 쓰기 잠금을 해제한다.
  3. ReadLock 메소드
    • 쓰기 잠금이 설정되어 있지 않을 때, 읽기 잠금을 1씩 증가시킨다. _flag의 하위 16비트를 READ_MASK로 확인하고, 해당 값에 1을 더해 Interlocked.CompareExchange로 갱신한다.
    • MAX_SPIN_COUNT만큼 시도한 후에도 잠금을 얻지 못하면 다시 반복한다.
  4. ReadUnlock 메소드
    • Interlocked.Decrement를 사용해 _flag의 읽기 잠금을 1 감소시켜 읽기 잠금을 해제한다.

코드의 핵심 동작

이 클래스는 쓰기와 읽기 잠금을 동시에 관리할 수 있는 간단한 락 시스템이다. 쓰기 잠금은 한 번에 한 스레드만이 설정할 수 있으며, 읽기 잠금은 쓰기 잠금이 걸려 있지 않은 경우에 여러 스레드가 동시에 설정할 수 있다. Interlocked 메소드는 원자적 작업을 보장하여 여러 스레드가 _flag 변수에 안전하게 접근할 수 있도록 한다.

'공부 > 게임서버' 카테고리의 다른 글

경합 조건 (Race Condition), Interlocked  (0) 2024.11.11
메모리 영역과 스레드  (0) 2024.11.11
임계영역 (Critical Section)  (0) 2024.11.10
가시성 문제와 보장 방법  (0) 2024.11.10
SpinLock / Interlocked.Exchange  (0) 2024.11.10