본문 바로가기
cs공부/운영체제

운영체제 - ReaderWriterLockSlim(RWLock), 사용자 정의 Lock구현 해보기

by 코딩하는 돼징 2023. 6. 1.
반응형

ReaderWriterLockSlim 클래스

동시에 여러개의 쓰레드가 공유 자원을 읽을 수 있지만 쓰기 작업이 수행되는 동안에는 상호 배타적으로 막는다.


01 EnterReadLock

읽기 잠금을 획득

public void EnterReadLock ();

02 EnterWriteLock 

쓰기 잠금을 획득

public void EnterWriteLock ();

03 ExitReadLock

ReadLock을 해제 한 후 다른 쓰레드가 write에 액세스 할 수 있도록 한다. 여러번 획득 한 경우 동일한 수의 ExitReadLock이 필요하다.

public void ExitReadLock ();

04 ExitWriteLock

WriteLock을 보유한 쓰레드가 lock을 해제하여 다른 쓰레드가 액세스 할수 있도록 한다. 

public void ExitWriteLock ();

2. 사용자 정의 Lock함수 구현하기 (재귀 X)

01 flag변수(32비트로 구성된 정수)

int flag = EMPTY_FLAG;

EMPTY_FLAG

비트 값이 모두 0으로 초기화된 상태

const int EMPTY_FLAG = 0x00000000;

WRITE_MASK

flag변수에서 WriteThreadId(쓰레드의 식별자)를 표시하는데 사용된다.

const int WRITE_MASK = 0x7FFF0000;

READ_MASK

ReadCount를 표현하는데 사용된다. ReadCount는 ReadLock을 획득한 쓰레드의 수를 나타낸다.

const int READ_MASK = 0x0000FFFF;

02 WriteLock

const int MAX_SPIN_COUNT = 5000;
int flag = EMPTY_FLAG;
public void WriteLock()
{
    // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다.
    int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
    
    // 락이 획득할때 까지 반복된다.
    while(true)
    {
        
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            // CompareExchange 메서드를 사용하여 flag 변수와 EMPTY_FLAG를 비교하고, 두 값이 같을 경우 desired 값으로 변경한다.
            // 변경에 성공하면 원래 flag 값을 반환하고, 실패하면 현재 flag 값을 반환한다.
            if (Interlocked.CompareExchange(ref flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                return;
        }
         // 일정 횟수의 시도가 실패한 경우, 다른 스레드에 양보(Yield)하여 CPU 자원을 더 공정하게 분배한다.
        Thread.Yield();
     }
}

코드 설명

1) desired 변수

int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;

Thread.CurrentThread.ManagedThreadId의 값을 desired변수의 WriteThreadId에 위치 시키기 위해 << 16 shift를 진행한다.


2) Writelock loop 설명

CompareExchange를 통해 lock 획득을 시도 한다.

if (Interlocked.CompareExchange(ref flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
    return;

flag값과 EMPTY_FLAG값을 비교하고 값이 같다면 flag에 desired값을 대입하고 반환 값은 원래 flag값이 반환된다.

반환된 값이 EMPTY_FLAG와 값이 일치하면 lock을 획득하였음을 의미하고 return되면서 메서드가 종료된다. 


3) Yield

스핀 횟수가 MAX_SPIN_COUNT보다 초과될 경우 양보하여 다른 쓰레드가 사용할 수 있도록 한다.


03 WriteUnlock

flag의 값을 EMPTY_FLAG로 변경하면서 락을 해제한다.

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

04 Readlock

 public void Readlock()
{
    while(true)
    {
        for(int i = 0; i<MAX_SPIN_COUNT;i++)
        {
            int expected = (flag & READ_MASK);    
            // 아무도 WriteLock을 획득하고 있지 않을 때, ReadCount를 1 늘린다.
            if (Interlocked.CompareExchange(ref flag, expected + 1 , expected) == expected)// A(0->1)승자    B(0->1) 실패 -> 다시시도
                return;
        }
        Thread.Yield();
    }
}

코드설명

1)  expected 변수

WRITE_MASK가 없다고 가정한 값

int expected = (flag & READ_MASK);

2) ReadLock loop

CompareExchange를 통해 lock 획득을 시도 한다. 

if (Interlocked.CompareExchange(ref flag, expected + 1, expected) == expected)
    return;

Lock 획득 성공한 경우

flag값과 expected값을 비교하고 값이 같다면 flag를 expected + 1로 바꿔 ReadCount를 1증가 시킨다. ReadLock을 획득하였으므로 함수를 종료하여 Read작업을 수행하도록 한다.

Lock 획득 실패한 경우

두 쓰레드가 동시에 획득하려는 상황에서 획득을 실패한 쓰레드는 다시 시도 해야 한다.


05 ReadUnlock

flag의 하위 16비트가 ReadCount이므로 값을 1 감소 시키면서 lock을 해제 한다. 

public void ReadUnLock()
{
    Interlocked.Decrement(ref flag);
}

3. 사용자 정의 Lock함수 구현하기 (재귀 O)

WriteLock -> WriteLock(OK), WriteLock -> ReadLock(OK), ReadLock -> WriteLock(No)

01 재귀 X와 WriteLock에서 다른 부분

int writeCount = 0;// 몇번 재귀되었는지 횟수
public void WriteLock()
{
    // 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
    int lockThreadId = (flag & WRITE_MASK) >> 16; // Id추출
    if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
    {
        writeCount++;
        return;
    }    
}

현재 쓰레드와 lockThreadId가 동일한 경우 writeCount를 증가시키고 함수를 반환한다. 이는 쓰레드가 여러 번 WriteLock을 호출할 때 재귀적으로 Lock을 획득한다.

public void WriteLock()
{
    while(true)
    {
        
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {

            if (Interlocked.CompareExchange(ref flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
            {
               writeCount = 1;
               return;
            }
        }   
     }
}

재귀적으로 호출되는 경우가 아닌 경우는 writeCount를 1로 설정한다.


02 재귀 X와 UnWriteLock에서 다른 부분

WriteLock을 반납하면서 writeCount값을 감소시키고, 모든 WriteLock이 반납되었을때 flag값을 초기화한다.

public void WriteUnlock()
{
    int lockCount = --writeCount;
    if(lockCount == 0)
    {
        Interlocked.Exchange(ref flag, EMPTY_FLAG);
    }
}

03 재귀 X와 ReadLock에서 다른 부분

flag의 하위 16비트가 ReadCount이므로 값을 1 증가시킨다.

public void Readlock()
{
    // WriteLock을 소유한 쓰레드 가져오기
    int lockThreadId = (flag & WRITE_MASK) >> 16;
    if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
    {
        // readCount를 증가시킨다.
        Interlocked.Increment(ref flag);
        return;
    }
}

1) lockThreadId 변수

WriteLock을 획득한 쓰레드의 Id

2) WriteLock을 이미 획득한 쓰레드인지 확인

if (Thread.CurrentThread.ManagedThreadId == lockThreadId)

현재 실행중인 쓰레드의 ManagerThreadId가 lockThreadId와 같은지 비교한다. 현재 쓰레드가 WriteLock을 이미 획득한 쓰레드인지 확인 하는 용도이다.


이를 확인 하는 이유

ReadLock은 은 여러 쓰레드가 동시에 획득할 수 있지만 WriteLock은 해당 쓰레드가 작업을 완료하기 전까지 다른 쓰레드들이 ReadLock을 획득해서는 안된다. 그러므로 WriteLock을 확득한 쓰레드와 동일한 쓰레드만이 ReadLock을 획득할 수 있다.


실행 코드 예시

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{

    class Program
    {
        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< 100; i++)
                {
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                }
            });
            Task t2 = new Task(delegate ()
            {
                for (int i = 0; i < 100; i++)
                {
                    _lock.WriteLock();
                    count--;
                    _lock.WriteUnlock();
                }
            });
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);
            Console.WriteLine(count);
        }
    }
}

결과 0이 잘 나오는것을 확인할 수 있다.

 

 

 

 

 

참고 :  본 내용은 MMORPG  PART4 강의를 수강하여 작성하였습니다.

https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4

반응형

댓글