[C#] Lock 구현

수민·2023년 6월 6일
0

게임서버 이론

목록 보기
3/6
post-thumbnail

인프런 강의 를 듣고, 개인 공부 목적으로 정리한 글입니다.


SpinLock

동작

끝날 때까지 계~ 속 기다린다. just 존버

최악의 경우,, 매우 오랜 시간 기다려야 하므로 (Busy Waiting)
엄청난 성능 저하CPU 낭비가 발생할 수 있다.

그래서, 금방 끝날 것 같은 상황에만 사용해야 한다.
대부분의 MMO 서버에서는 SpinLock을 사용하는게 일반적이다.

구현

기본적으로 CAS (Compare-And-Swap) 을 사용해서 구현한다.

namespace Ifrn_ServerCore
{
    class SpinLock
    {
        volatile int _locked = 0;

        public void Acquire()
        {
            int expected = 0;
            int desired = 1;
            while(Interlocked.CompareExchange(ref _locked, desired, expected) == desired);
        }

        public void Release() 
        {
            _locked = 0;
        }
    }
    ...
}

CAS (Compare-And-Swap)

말 그대로 비교 후 교체를 의미한다.
비교하고 교체하는 작업을 lock-free하게 동작한다.

lock-free
딜레이될 수는 있지만,
여러개의 쓰레드가 동시에 실행했을 때 한 개 이상의 쓰레드가 반드시 종료한다.

C#의 Interlocked 클래스에 CompareExchange 메서드가 있다.
C++과는 좀 다를 수 있는데,
Interlocked.CompareExchange(ref value, desire, expected) 의 동작은

if (value == expected)
	value = desire

을 의미한다.
리턴값은 그 이전 값이 나온다.


Context Switching

동작

Context Switching이라는게.. Lock에서 나온 개념은 아니고
Lock을 구현하는 방법에서 Context Switching을 사용한다는 의미에용.

Context Switching

= CPU가 작업 A를 중지하고 작업 B를 수행하도록 하는 것
작업 A와 관련된 정보들은 따로 저장해놓고,
작업 B와 관련된 정보를 메모리에서 가져와 레지스터에 올린다.
Kernel에 접근한다.
프로세스 간의 Context Switching도 있고,
쓰레드 간의 Context Switching도 있다.
(프로세스는 1개 이상의 쓰레드로 구성된다)

위에서 말했듯, Context Switching은 Kernel에 접근한다.
그래서 성능 저하의 요인이 된다.

CPU는 한 번에 하나의 프로세스만 실행할 수 있다.
그래서
Context Switching은 당연히 일어난다.

하지만
이렇게 Lock을 구현할 때 차례를 기다리기 위해서 성능 저하의 요인인 Context Switching을 사용한다면, 당연히 성능은 느려질 수 밖에 없다.

그래서
Lock의 점유 시간이 있을 때만 사용하자.

구현

SpinLock기반으로
Acquire 하지 못한 경우에
CPU 소유권을 포기하고 다른 쓰레드에게 양보해준다.

양보해주는 방법은 3가지가 있다.

Thread.Sleep(1);		// 정수
Thread.Sleep(0);
Thread.Yield();

Thread.Sleep(정수) : 무조건 휴식. 정수ms만큼 쉰다
Thread.Sleep(0) : 조건부 휴식
우선순위가 자신 이상인 쓰레드에게 소유권을 돌리고, 없으면 다시 본인에게 돌아온다.
Thread.Yield() : 관대한 양보 : 지금 실행 가능한 쓰레드가 있으면 실행. 없으면 다시 본인에게 돌아온다


Event

사실 Event라는게
뭔가 했으면 나 했어요!!!!!하고 알려주는걸 의미한다.

"@가 @@하면 저한테 알려주세요 ~" 라고 Kernel한테 시키고 자기는 다른 일을 한다. 그리고 Kernel은 해당 일이 일어나면 알려준다.

Event에는 두 종류가 있다.

Auto Reset Event

내가 입장하면 자동으로 문을 닫아준다고 생각하면 될 것 같다.

 AutoResetEvent _available = new AutoResetEvent(true);

public void Acquire()
{
    _available.WaitOne(); 
}

public void Release() 
{
	_available.Set(); 
}

AutoResetEvent _available : 접근 가능한지 여부를 의미하는 객체로,
Kernel에서는 bool로 인식한다.

WaitOne() : 입장을 시도한다.
Set() : Event의 상태를 true로 바꾼다.

사실
Reset() : Event의 상태를 false로 바꾼다.
라는 메서드도 있는데

AutoResetEvent에서는 WaitOne()안에 Reset()이 자동으로 포함되어 있다.

그래서 이렇게 구현하면 쉽게 동작한다.

Manual Reset Event

Manual Reset Event에서는 Reset을 수동적으로 해줘야 한다.

ManualResetEvent _available = new ManualResetEvent(true);

public void Acquire()
{
    _available.WaitOne(); 
    _available.Reset();
}

public void Release() 
{
	_available.Set(); 
}

사실
Acquire에서
WaitOne()Reset()이 두 라인으로 개별로 실행된다.
Reset()을 하기 전까지는 다른 스레드들이 접근할 수 있다.
이렇게 되면 Lock을 구현할 수가 없다.

ManualResetEvent는 Lock이 아니라
어떠한 일이 발생했다고 알려주는 용도로 사용하자.

profile
우하하

0개의 댓글