1. SpinLock
Lock을 얻기 위해 계속해서 Lock 획득을 할 때 까지 루프를 돌면서 수행하는 방식으로 하나의 쓰레드가 Lock을 오랜시간동안 점유하고 있다면 다른 쓰레드는 Lock을 획득하기 위해서 루프를 돌면서 Lock이 풀렸는지 계속 확인하므로 CPU에 부담이 가해진다.
SpinLock 코드
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore {
class SpinLock
{
// volatile 키워드를 사용해서 가시성 역할 수행
volatile int _locked = 0;
public void Acquire()
{
// 1. lock이 풀릴 때까지 계속 기다림
// ex) 화장실에서 사람이 나올 때까지 계속 기다림
/*while (_locked)
{
}*/
// 2. lock을 획득
// _locked = 1;
// 1번과 2번을 따로 실행하게 되면 두 개 이상의 쓰레드가 동시에 접근하게 될 때 문제가 발생
// 1번에 동시에 접근해서 통과한 이후 여러개의 쓰레드가 lock을 가지게 될 수 있음
// 해결 방법으로는 1번과 2번 작업을 하나의 작업단위 즉, 원자성 단위로 수행
while (true)
{
// 버전1)
// lock이 0이라면(풀려있다면) 1로 변경(lock 획득 후 반복문 탈출)
/*
int original = Interlocked.Exchange(ref _locked, 1);
if (original == 0) {
break;
}
*/
// 버전2)
// 직관적으로 이해하기 쉬운 버전
// 가독성을 높이기 위해서 0과 1을 의미있는 표현으로 변경함
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
{
break;
}
}
}
public void Release()
{
// lock을 해제
// ex) 볼일을 다보고 화장실 문을 열음
_locked = 0;
}
}
class Program
{
static int num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for(int i = 0; i < 10000; i++)
{
_lock.Acquire();
num++;
_lock.Release();
}
}
static void Thread_2()
{
for(int i = 0; i < 10000; i++)
{
_lock.Acquire();
num--;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(num);
}
}
}
2. 다른 작업을 하다가 설정한 시간 뒤에 Lock을 체크하는 방법
해당 쓰레드가 해당 자원의 Lock을 얻기 위해서 접근하는 작업을 하고 있는데 이미 Lock을 누군가 점유하고 있어서 잠시 Lock을 체크하는 작업을 중단하고 다른 작업을 처리하다가 다시 Lock을 체크하는 방식
이러한 Context Switching(컨텍스트 스위칭)이 여러번 발생하면 오버헤드가 발생해서 컴퓨터 성능이 저하될 수 있음
public void Acquire()
{
while (true)
{
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
{
break;
}
// Lock을 획득 할 수 없으면 Lock 체킹 반복작업을 중지하고 다른일을 처리함
Thread.Sleep(1); // 무조건 휴식
Thread.Sleep(0); // 조건부 양보(우선순위)
Thread.Yield(); // 실행가능한 쓰레드가 있으면 양보하고 없으면 자기가 남은 시간동안 점유
}
}
1~2. 소설책을 읽고 있다가 만화책을 읽고 싶어서 어디까지 읽었는지에 대한 정보를 RAM에 저장
3. RAM에서 만화책을 읽는 작업을 꺼내와서 실행 대기
4. 만화책을 읽는 작업을 수행
5. 만화책이 질려서 어디까지 읽었는지에 대한 정보를 RAM에 저장
6. 소설책을 읽는 작업을 수행
이렇게 기존에 실행중인 프로세스를 중단하고 다른 프로세스가 실행되는 과정이 많이 발생하면 소설책만 계속 읽는 것보다 작업속도가 느려질 수 있음
3. AutoResetEvent
해당 공유자원을 접근하려고 할 때 Lock이 풀렸는지 확인하기 위해서는 커널단계까지 내려가서 체크해야되기 때문에 "SpinLock" 계열의 Lock에 비해서 시간이 오래걸린다.
Lock이 금방 풀렸다가 다시 Lock을 해야하는 공유자원보다는 오랫동안 공유자원을 점유하는 해야하는 경우에 효율적이다. Lock을 체크하기 위해서 커널단계까지 내려가는데 시간이 오래걸리기 때문
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Lock
{
// "_available"은 커널에서 관리되는 Lock
// 해당 자원에 접근할때 Lock이 풀렸는지 확인하기 위해서는 커널단계까지 내려가서 체크해야 되기 때문에 시간이 오래걸림
AutoResetEvent _available = new AutoResetEvent(true);
public void Acquire()
{
_available.WaitOne();
}
public void Release()
{
_available.Set();
}
}
class Program
{
static int num = 0;
static Lock _lock = new Lock();
static void Thread_1()
{
for (int i = 0; i < 100000; i++)
{
_lock.Acquire();
num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Acquire();
num--;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(num);
}
}
}
'게임개발 > 게임서버' 카테고리의 다른 글
메모리 배리어 (0) | 2024.04.18 |
---|---|
멀티 쓰레드 프로그래밍 (0) | 2024.04.17 |
소켓 프로그래밍 (0) | 2023.10.29 |
TLS(Thread Local Storage) (0) | 2023.10.28 |
ReadWriteLock (0) | 2023.10.26 |