📄 ReaderWriterLock

ReaderWriterLock은 이전 글[유니티 서버 구축(C#) 9.ReaderWriterLock]에서 다뤘습니다. 상호배제를 위한 기능은 동일하지만, 데이터에 대해 읽기, 쓰기에 대한 호출의 비율이 매우 편중되어있을 때 적합한 방법입니다.

 

📑 Lock.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    //재귀적 락을 허용하지 않는 모델
    class Lock
    {
        //음수가 될 수 있기에 최상위비트 사용하지 않음
        const int EMPTY_FLAG = 0x00000000;

        //마스크를 사용하여 필요한 영역만 빠르게 추출하기 위해 사용
        const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
        const int READ_MASK = 0x0000FFFF; //16자리

        //스핀락 정책: 5000번 시도 후 yield
        const int MAX_SPIN_COUNT = 5000;

        //int형 비트를 다음과 같이 사용: [Unused(1)] [WriteThreadID(15)] [ReadCount(16)]
        //WriteThreadID는 쓰기작업을 하여 Lock을 하는 스레드ID
        int mFlag = EMPTY_FLAG;
        int mWriteCount = 0;

        /// <summary>
        /// 아무도 WriteLock 또는 ReadLock을 획득하고 있지 않을때 경합해서 소유권을 획득
        /// </summary>
        public void WriteLock()
        {
            //동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
            int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득

            //스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
            if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
            {
                ++mWriteCount;

                return;
            }

            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; ++i)
                {
                    //스레드 ID를 얻어 16비트만큼 Left-Shift하여 위치시키고
                    //WRITE_MASK와 AND연산으로 다른 영역의 비트를 모두 0으로 한 후에 mFlag에 삽입
                    if (Interlocked.CompareExchange(ref mFlag, (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK, EMPTY_FLAG) == EMPTY_FLAG)
                    {
                        mWriteCount = 1;

                        return;
                    }
                }

                //5000번 시도 후 소유권 획득 실패시 yield
                Thread.Yield();
            }
        }

        public void WriteUnlock()
        {
            int lockCount = --mWriteCount;

            //Write락을 언락할때, 락카운트가 0이 되는 경우에만 락해제 가능
            if (lockCount == 0) { Interlocked.Exchange(ref mFlag, EMPTY_FLAG); }
        }

        /// <summary>
        /// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1올린다.
        /// </summary>
        public void ReadLock()
        {
            //동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
            int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득

            //스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
            if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
            {
                ++mFlag;

                return;
            }

            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; ++i)
                {
                    int expected = mFlag & READ_MASK;

                    //만약 mFlag가 expected라면, 아무도 WriteLock을 사용하고 있지 않음(WRITE_MASK는 0)

                    //expected가 1을 늘리는 도중(expected + 1)에 어딘가에서 1을 이미 올렸다면 expected가 아닐 수 있음
                    if (Interlocked.CompareExchange(ref mFlag, expected + 1, expected) == expected) { return; }
                }

                //5000번 시도 후 소유권 획득 실패시 yield
                Thread.Yield();
            }
        }

        public void ReadUnlock()
        {
            //Read가 끝나 Unlock을 할때 1을 감소하도록 함
            Interlocked.Decrement(ref mFlag);
        }
    }
}
  • ReaderWriterLock을 구현한 Lock 클래스입니다.
  • 재귀적 락을 허용하는 모델입니다.

 

const int EMPTY_FLAG = 0x00000000; //음수가 될 수 있기에 최상위비트 사용하지 않음
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
const int READ_MASK = 0x0000FFFF; //16자리

int mFlag = EMPTY_FLAG; //사용할 플래그 정수(비트로 연산)

 

  • mFlag를 이용하여 락에 대한 연산을 수행합니다.
  • 정수 값이 아닌, 내부의 비트(32 bits)를 이용하여 계산합니다.

 

const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
  • WRITE_MASK는 WriterLock을 연산하기위한 마스크입니다.
  • 총 15자리를 사용하며 최상위비트를 제외합니다. signed integer에서 최상위비트가 1이면 음수로 바뀌면서 비트가 변하기 때문에 이를 방지하고자 하나의 비트를 제외합니다.

 

const int READ_MASK = 0x0000FFFF; //16자리
  • READ_MASK는 ReaderLock을 연산하기위한 마스크입니다.

 

const int MAX_SPIN_COUNT = 5000;
  • 스핀락 사용하여 5000번의 획득시도 중 실패할경우, yield 하도록 구현합니다.

 

public void WriteLock()
  • 재귀적 락을 허용하기위해 먼저 lockThreadID를 구합니다. WRITE_MASK에서 나온 값을 ID와 비교하기 위해 RShift를 하여 비트위치를 맞춥니다.
  • if (Thread.CurrentThread.ManagedThreadId == lockThreadID)가 참이라면. 락을 하고 있는 스레드가 또다시 WriteLock을 시도하는 것으로 mWriteCount를 1 증가시켜 줍니다.
  • 다른 스레드라면, 5000번의 시도를 하여 mFlag가 EMPTY_FLAG인지 확인합니다.
  • 만약 EMPTY_FLAG라면 아무도 Lock을 사용하지 않기 때문에 자신의 스레드 ID로 mFlag의 값을 바꿔줍니다.

 

public void WriteUnlock()

 

  • WriteLock을 해제합니다.
  • 재귀 락에 대응하기 위해 mWriteCount를 1씩 감소시켜 lockCount(감소된 Lock 수)가 0이라면 mFlag를 EMPTY_FLAG로 바꿔줍니다.

 

public void ReadLock()
  • ReadLock을 합니다.
  • 이미 동일한 스레드가 락을 하고 있는 경우에, 이미 락을 획득한 스레드이기 때문에 ++mFlag를 합니다.
  • 5000번의 시도를 하여 락을 획득하고자 합니다. mFlag & READ_MASK가 mFlag와 같다면, 현재 WriteLock은 없는 상태이기에 ReadLock을 획득할 수 있습니다.

 

public void ReadUnlock()
  • ReadLock을 해제합니다.

 

📑 Main.cs

namespace ServerCore
{
    class Program
    {
        static volatile int count = 0;
        static Lock _lock = new Lock();

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

            Task t2 = new Task(delegate ()
            {
                for (int i = 0; i < 10000000; ++i)
                {
                    _lock.WriteLock();
                    --count;
                    _lock.WriteUnlock();
                }
            });

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

            Task.WaitAll(t1, t2);

            Console.WriteLine(count);    
        }
    }
}
  • 두 개의 태스크를 생성하여 두개의 스레드에서 +, - 연산을 하여 0이 나오는지 확인해 봅니다.

'server > socket server' 카테고리의 다른 글

[C# 서버] 소켓 프로그래밍  (0) 2023.01.14
[C# 서버] Thread Local Storage  (0) 2023.01.12
[C# 서버] ReaderWriterLock  (0) 2023.01.10
[C# 서버] Mutex  (0) 2023.01.09
[C# 서버] AutoResetEvent  (1) 2023.01.06
bonnate