C#/네트워크

[c#][서버] ReaderWriterLock

goliot 2024. 6. 3. 16:12
반응형

C# 내장 SpinLock

static object _lock = new object();

static void Main(string[] args)
{
    bool lockTaken = false;
    try
    {
        _lock2.Enter(ref lockTaken);
    }
    finally
    {
        if (lockTaken) _lock2.Exit();
    }
}
//내부적으로 무한 뺑뺑이가 아닌, 너무 오래걸린다면 양보를 하도록 설계됨
  • 이전에 직접 구현해 본 spinlock은 c#에 자체적으로 위와 같이 존재함
  • 어떤 락이든, 기본 철학은 상호 배제

ReaderWriterLock

  • 읽기는 락을 안걸고, 쓸 때만 락을 걸고 싶다면?
    • 즉, 평소에는 락이 필요 없는데 특수한 경우에만 락이 필요한 경우
class Reward
{

}

//선언 형식
static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();

//자주 호출되는 함수
static Reward GetRewardById(int id)
{
    _lock3.EnterReadLock();
    //여러 쓰레드가 동시 접근은 가능하지만, 쓰기 작업은 차단
    _lock3.ExitReadLock();

    return null;
}

//일주일에 한번 호출되는 함수
static void AddReward(Reward reward)
{
    _lock3.EnterWriteLock();
    //단 하나의 쓰레드만 접근 가능, 쓰기 가능
    _lock3.ExitWriteLock();
}
  • 이렇게 Read와 Write의 락을 구분하여 각 용도에 맞게 사용
    • EnterReadLock() : 여러 쓰레드의 접근은 허용하지만, 쓰기 작업은 차단
    • EnterWriteLock() : 하나의 쓰레드만 접근 허용하며, 쓰기 작업도 가능

ReaderWriterLock 직접 구현해보기(SpinLock 방식으로)

  • 재귀적 락을 허용하지 않는 버전 ↓
// 재귀적 락을 허용할지 (No)
    //락을 얻은 쓰레드에서 한번 더 락을 얻는 것을 허용할 것인지
// 스핀락 정책 (5000번 -> Yield)
class Lock
{
    const int EMPTY_FLAG = 0x00000000; //첫 1비트 // 0000 0000 0000 0000 0000 0000 0000 0000
    const int WRITE_MASK = 0x7FFF000; //두번째부터 15비트 //0111 1111 1111 1111 0000 0000 0000 0000
    const int READ_MASK = 0x0000FFFF; //뒤 16비트 // 0000 0000 0000 0000 1111 1111 1111 1111
    const int MAX_SPIN_COUNT = 5000;

    // [Unused(1bit)] [WriteThreadId(15bit)] [ReadCount(16bit)]
    int _flag = EMPTY_FLAG;

    public void WriteLock()
    {
        //ThreadID를 가져와서, 15비트를 저장하는 방법
        //16비트를 밀고 WRITE_MASK와 & 연산을 통해 범위를 넘어가지 않도록 설정
        int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;

        // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 소유권을 경합해서 얻는다
        while(true)
        {
            for (int i = 0; i < MAX_SPIN_COUNT; i++)
            {
                //시도를 해서 성공하면 return
                if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                    return;
                //여러개가 동시에 들어와도 ID가 다르기 때문에 누군가 승자는 존재함

                /*if(_flag == EMPTY_FLAG)
                {
                    _flag = desired;
                    return;
                }*/
            }

            Thread.Yield(); //5000번 하고 실패시 양보
        }
    }

    public void WriteUnlock()
    {
        //저장된 Thread ID를 밀어버리면 된다
        Interlocked.Exchange(ref _flag, EMPTY_FLAG);
    }

    public void ReadLock()
    {
        // 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1 늘린다
        // 여러 쓰레드가 동시에 Read를 잡을 수 있게 하기 위함
        while(true)
        {
            for(int i=0; i<MAX_SPIN_COUNT; i++)
            {
                int expected = (_flag & READ_MASK); //WRITE 부분을 0으로 밀어버린 값이 기대값
                if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                    return;
                //A, B가 동시에 입장 했을 때
                //A가 승자라고 하면, ReadCount를 0 -> 1로 바꿈
                //B가 요청하려 할 때, expected가 0이어야 하는데, 실제 값은 1이 나옴
                //for문 다음 반복으로 돌림
                //돌리면 expected가 1이 됨, 이제 맞으므로 1 -> 2 연산을 수행함

                /*if((_flag & WRITE_MASK) == 0) //& 연산하고 0이라면, 아무도 WRITE를 가지고 있지 않다는 뜻
                {
                    _flag = _flag + 1;
                    return;
                }*/
            }

            Thread.Yield();
        }
    }

    public void ReadUnlock()
    {
        Interlocked.Decrement(ref _flag); //Read Count 1 줄이기
    }
}
  • 재귀적 락을 허용하는 버전 ↓
// 재귀적 락을 허용
// WriteLock -> WriteLock, WriteLock -> ReadLock (o)
//ReadLock -> WriteLock (x)
class Lock
{
    const int EMPTY_FLAG = 0x00000000; //첫 1비트 // 0000 0000 0000 0000 0000 0000 0000 0000
    const int WRITE_MASK = 0x7FFF000; //두번째부터 15비트 //0111 1111 1111 1111 0000 0000 0000 0000
    const int READ_MASK = 0x0000FFFF; //뒤 16비트 // 0000 0000 0000 0000 1111 1111 1111 1111
    const int MAX_SPIN_COUNT = 5000;

    // [Unused(1bit)] [WriteThreadId(15bit)] [ReadCount(16bit)]
    int _flag = EMPTY_FLAG;
    int _writeCount = 0; //재귀 write를 위한 카운터 추가

    public void WriteLock()
    {
        //동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
        int lockThreadId = (_flag & WRITE_MASK) >> 16;
        if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
        {
            _writeCount++; //이미 갖고 있다면 count만 올려주면 끝
            return;
        }

        int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
        while(true)
        {
            for (int i = 0; i < MAX_SPIN_COUNT; i++)
            {
                if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                {
                    _writeCount = 1;
                    return;
                }
            }

            Thread.Yield();
        }
    }

    public void WriteUnlock()
    {
        //락 카운트를 unlock 할 때마다 1씩 줄이다가
        int lockCount = --_writeCount; 
        if (lockCount == 0) //그게 마지막 락이었다면, Release
            Interlocked.Exchange(ref _flag, EMPTY_FLAG);
    }

    public void ReadLock()
    {
        int lockThreadId = (_flag & WRITE_MASK) >> 16;
        if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
        {
            Interlocked.Increment(ref _flag); //리드카운트 늘리기
            return;
        }
        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;
            }

            Thread.Yield();
        }
    }

    public void ReadUnlock()
    {
        Interlocked.Decrement(ref _flag); //Read Count 1 줄이기
    }
}
  • 이 방식을 사용 할 때에 주의할 점!
    • Write -> Read순서로 Lock을 얻었다면, Read -> Write 순서로 Unlock 해야 함

구현한 ReaderWriterLock 테스트

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.WriteLock();
            count--;
            _lock.WriteUnlock();
        }
    });

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

    Task.WaitAll(t1, t2);

    Console.WriteLine(count);
}

잘 나온다!

 

반응형

'C# > 네트워크' 카테고리의 다른 글

[c#][서버] 네트워크 기초 이론  (0) 2024.06.04
[c#][서버] Thread Local Storage  (0) 2024.06.03
[c#][서버] Lock 구현 이론  (0) 2024.06.02
[c#][서버] 데드락  (0) 2024.06.02
[C#][서버] Lock 기초  (0) 2024.06.02