Server programming

01_13_멀티쓰레드_ReaderWriteLock

devRiripong 2023. 3. 21.
반응형

Q1. 지난 시간에 알아본 기본 락 세가지 형태에 대해 설명해 보세요

Q2. 실용성있는 락 3가지를 말하고 설명해 보세요

 

 

이전시간 까지 락을 구현하는 세가지 방법에 대해 얘기 했었어.

잠시 복습

  1. 근성
  2. 양보
  3. 갑질

어떤게 더 좋다는 개념은 없어.

락을 사용할 때 하나만 골라서 쓰지 않고 대부분 라이브러리에서 여러가지 혼합해서 사용하는 경우가 많다.

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

namespace ServerCore
{
    internal class Program
    {
        // 1. 근성
        // 2. 양보
        // 3. 갑질

        // 내부적으로 Monitor를 이용
				
				// 버전1
        static object _lock = new object();
        
				// 버전2
        static SpinLock _lock2 = new SpinLock(); 
				
				// 버전3 직접 만든다

        static void Main(string[] args)
        {
            lock (_lock)
            {
            
            }

            bool lockTaken = false;
            try
            {
                _lock2.Enter(ref lockTaken);
            }
            finally
            {
                if(lockTaken)
                    _lock2.Exit(); 
            }

        }
    }
}

락을 사용할 때 일단 오브젝트(위 코드에서 _lock)를 만들어서 lock 키워드를 이용해 사용하는 방법이 있었고,

그 다음은 SpinLock인데 지난번엔 직접 구현했지만 SpinLock을 쳐보면 이미 구현된게 있다. 안만들고 얘를 사용하면 된다. try안에서 정상적으로 처리가 안됐을 경우 처리하기 위해 lockTaken 안에 결과물을 넣어준다. 성공해서 lockTaken이 true가 되면 Exit를 실행해준다.

라이브러리로 제공하는 SpinLock의 문서를 읽어보면

1.근성 으로 계속 두드리는 방식으로 이루어져 있기는 하지만 몇번 시도를 하다가 답이 없다면 중간에 2. 양보 yield를 해서 자기 소유권을 포기하고 쉬다 온다고 한다.

1.근성 과 2.양보의 방법을 혼합한 형태라고 볼 수 있다. 중간중간에 쉬다 오는게 훨씬 좋다. lock을 획득하지 못했다는 건 상대방이 오래 잡고 있지 못했다는 것이기 때문에 쉬다 오는 게 좋다.

static Mutex _lock3 = new Mutex();

얘 같은 경우는 조금 많이 무겁다 했었어. 느리지만 단점만 있는 건 아냐.

장점은 같은 프로그램이 아니라 별도의 프로그램 끼리도 순서를 맞추는 동기화 작업을 할 때 사용할 수 있는 묘한 장점이 있기는 한데 MMORPG 서버는 프로그램 안에 멀티스레드로 돌아가는 거라 프로그램 사이에 동기화 할 수 있다는건 크게 와닿지 않는 장점이기에 그냥 잊어도 된다.

첫번째 버전(lock)을 쓰거나 두번째 버전(SpinLock)을 쓰거나 직접 만들어도 된다.

사람마다 다르지만 기존의 것을 활용해 조립하는 사람이 있는가 하면, 직접 만드는 걸 좋아하는 사람도 있다. 처음에는 만들다가 익숙해지면 있는거 쓰는게 좋다고 생각함.

참고로 lock(_lock)을 쓰는건 내부적으로 monitor를 쓴다고 했는데 얘는 3개중에 무엇인지 문서를 찾아봐도 안나타나있어. 크게 중요하진 않다.

상황에 따라 어떤건 좋고 어떤건 나쁘기 때문에 서버 올리고 테스트 해보고 너무 느리다 싶으면 수정해 테스트 하는 방식으로 진행하면 된다.

SpinLock 방식이 아쉬운게 깔끔하게 구현할 수 없다는게 아쉽다.

lock(_lock2) 이런 식으로 구현하는 건 불가능하다. 그래서 성능차이가 확연히 나는 건 아니라서 lock(_lock) 이 첫 버전을 이용하는 것도 나쁘지 않다.

서버를 쌓아 올리게 되면 멀티 스레드를 사용하게 되는데 만드는데 핵심 코어만 멀티스레드로 만들것인지 게임 컨텐츠 부분도 멀티 스레드로 만들지는 별개의 문제다. 이건 선택의 시간이 오게 된다.

일반적으로 컨텐츠를 멀티스레드로 만들면 난이도가 확연하게 올라간다.

장점은 신리스(경계가 없는) mmorpg 만들 때 이점이 있다. 일반적으로 바람의나라나 뮤같이 지도가 나눠져 있으면 싱글 스레드가 낫다.

어떻게 사용할지에 따라서 어떤 버전 사용할지 조건에 따라 갈린다.

락들의 내부적 구현, 실행속도는 다르겠지만 기본 원리는 같다. 한번에 한놈만 들여 보내겠다가 기본적 아이디어. 철학은 상호배제. 무조건 나만 들어갈 것이다가 기본 원리.

경우에 따라 다른 형태 락이 유용할 수 있다. 온라인 게임에서 일일퀘스트 했을 때 보상 받는다 해보자.

// [ ] [ ] [ ]

퀘스트 보상으로 3개를 기본적으로 받는데 나중에 운영팀에서 추가적으로 이벤트 보상을 줄 수 있다고 하면, 운영 툴로 2개 정도로 추가할 수 있는

// [ ] [ ] [ ] [ ] [ ]

class Reward
{

}

static Reward GetRewardById(int id)
{
    lock(_lock)
    {

    }
    return null; 
}

Reward 클래스에 보상과 관련된 내용이 있을 것이고, GetRewardById로 Reward를 찾는 거라고 해보자. 열심히 찾아서 Reward를 반환해 주는 게 있다고 가정을 해보면

운영툴로 보상을 중간 추가할 수 있으니까 결국에는 일종의 lock이 들어가야 해. lock을 걸자니 아쉬운 건 운영툴로 보상을 주는 건 거의 1주일의 1번 정도야. 가끔만 바뀌고 대부분의 경우에 변함이 없을텐데 0.0000001% 때문에 lock을 거는게 아쉬운 경우가 있다.

Get할 때는 동시 다발적으로 접근할 수 있게 하고, 쓸 때는 Overwirte해서 바꿀 때만 lock을 걸어 상호 베타적으로 막 수 있으면 효율적이란 생각이 들거야. 일반적인 99.9999999%의 경우에는 락이 없는 것처럼 쿨하게 왔다 갔다 하다가 특수한 경우에만 서로 막아 버리면 된다는 의미가 된다.

이렇게 특수한 방식으로 작동하는 락을 별도의 방식으로 부르는데

RWLock 혹은 ReaderWriteLock이라고 부른다.

C#에도 구현이 되어 있다.

2가지 버전이 있는데 Slim 붙은 애 가 더 최신 버전이야.

static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();

id 값을 이용해 어떤 Reward인지 조회하는 GetRewardById라는 함수가 있고,

그리고 실제로 운영툴에서 AddReward라는 식으로 보상을 추가할 수 있는 기능이 하나가 더 있다고 가정을 해볼거야.

// 99.999999
static Reward GetRewardById(int id)
{
    _lock3.EnterReadLock();  // 읽을 떄는 이 버전

    _lock3.ExitReadLock(); 

    return null; 
}

// 0.0000001%
static void AddReward(Reward reward)
{
		_lock3.EnterWriteLock();

    _lock3.ExitWriteLock();
}

대부분은 GetRewardById로 호출이 되고 극악의 확률로 일주일에 한번 호출 될까 말까로 AddReward로 호출이 된다고 보면 된다.

만약에 여러개의 스레드가 GetRewardById에 동시다발적으로 들어왔다고 가정을 하는데 아무도 WriteLock을 잡고 있지 않는다고 하면 _lock3는 동시 다발적으로 들어 올 수 있는 거 .

넓은 화장실이라 동시 다발적으로 들어오고 나갈 수 있는데

vip가 온다고 하면 다 쫒아내고 혼자만 사용할 수 있게 보장을 받게끔 하는 자본주의적인 화장실이라 생각하면 된다.

누가 WriteLock을 잡았다고 하면 ReadLock은 얼씬도 못하는 슬픈일이 될 것이다.

// 내부적으로 Monitor를 이용
static object _lock = new object();         // 기본적인 락   
static SpinLock _lock2 = new SpinLock();    // 스핀락

// RWLock, ReaderWriteLock
static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();    // Reader락

이 중에 하나를 골라서 사용하면 된다.

아직 컨텐츠가 많지 않으면 첫번째 버전이 lock(_lock) 이런식으로 편하게 사용할 수 있기 때문에 가장 우아한 방법이 되긴 할거야.

이렇게 해서 lock에 대한 내용은 끝났다.

다음 시간에는 멀티 스레드에 관한 연습으로 ReaderWriterLock을 간단하게 구현을 해보자. 대충 어떤 느낌으로 구현이 될 수 있을지 예측이 될 거 같고,

InterLocked.ComparedSwap계열 함수들 제대로 사용 안해봤는데 걔네들 중요하게 사용되기 때문에 친숙해지기 위해 구현해 볼거야.

 

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

namespace ServerCore
{
    internal class Program
    {
        // 1. 근성
        // 2. 양보
        // 3. 갑질

        // 내부적으로 Monitor를 이용
        static object _lock = new object();        
        static SpinLock _lock2 = new SpinLock();

        // RWLock, ReaderWriteLock
        static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();

        // 직접 만든다

        class Reward
        {

        }

        // 99.999999
        static Reward GetRewardById(int id)
        {
            _lock3.EnterReadLock();

            _lock3.ExitReadLock(); 
      
            return null; 
        }

        // 0.0000001%
        static void AddReward(Reward reward)
        {
            _lock3.EnterWriteLock();

            _lock3.ExitWriteLock(); 
        }
        
        static void Main(string[] args)
        {
            lock (_lock)
            {
            
            }

            bool lockTaken = false;
            try
            {
                _lock2.Enter(ref lockTaken);
            }
            finally
            {
                if(lockTaken)
                    _lock2.Exit(); 
            }

        }
    }
}

 

출처: https://inf.run/P4nt

반응형

댓글