<ReaderWriterLock>
1. 이전시간 Spinlock & Mutex
: Spinlock 또한 미리 구현된게 있기때문에 이전 시간처럼 Spin lock 클래스를 구현할 필요없이 바로 사용하면 됨
1) Enter() 함수를 통해 임계 구역에 들어갈 수 있도록 하고 Exit() 함수를 통해 빠져나오도록한다. 이때 결과값을 lockTaken 인자로 받아서 준다. * lockTaken으로 성공여부를 알 수 있다.(이미 임계 구역에 들어가있는 스레드가 있을 경우 false 반환)
2) try- finally~ 문을 사용하여 exception 발생 시 finally를 통해 locckTaken이 true 일시 잠금을 해제하도록 한다.
3) 기본적으로 Spinlock은 임계 구역에 들어가기 전까지 사용자 모드에서 계속 기다리지만, 정말 안될 시, yield로 자기 권한을 다른 스레드에게 양보한다.
4) Mutex의 경우, 프로그램들 사이에서 동기화 작업을 가능하게 하는 장점은 있지만 Mutex 자체가 무겁게 돌아가서 MMORPG 게임에서는 장점이 없다.
namespace SeverCore
{
class Program
{
static SpinLock _lock2 = new SpinLock();
static void Main(string[] args)
{
bool lockTaken = false;
try
{
_lock2.Enter(ref lockTaken); // 예상치 못한 exception이 발생했을 때를 대비하여 성공 여부를 lockTaken에 반환
}
finally
{
if(lockTaken)
_lock2.Exit();
}
}
}
}
2. 직접 만든다. * 초기에는 직접 만드는 것이 좋음
아래의 lock은 1. 근성, 2. 양보 3. 이벤트의 DeadLock 상황 방지를 위한 방법 중 아무 것에도 포함되어 있지 않다.
서버에 올린 후 상황을 보고 lock을 적용할지, SpinLock을 적용할지 결정한다.
static object _lock = new object();
static void Main(string[] args)
{
lock(_lock)
{
}
3. 핵심 부분만 멀티 쓰레드로 만들 것인지 게임 컨텐츠 자체를 멀티 쓰레드로 만들 것인지
: 일반적으로 컨텐츠를 멀티스레드로 만들면 난이도가 확연히 올라간다. 장점은 심리스 MMORPG를 만들때 좋다. 일반적으로 공간이 분리가 되어있고 나뉘어져있는 게임에서는 모든 컨텐츠 코드는 싱글 스레드로 하면 시간 절약이 된다.
4. ReaderWriteLock
: 기본적인 Lock의 철학은 상호배제, 경우의 달라질 수 있음
ex) 게임에서 일일 퀘스트의 보상으로 아이템을 3 개를 받는데 추가적으로 보상을 2 개를 더 받는 것이 가능하다고 가정
-> 2개의 아이템은 운영툴을 만들어서 추가
1) 아래의 코드와 같이 GetRewardByld 코드를 통해 보상을 반환한다고 했을때, 운영툴로 인해 추가 보상이 있을 수도 있기 때문에 lock으로 감싸서 상호배제를 해야한다.
class Reward
{
}
static Reward GetRewardByld(int id)
{
lock (_lock)
{
}
return null;
}
2) 하지만 이러한 추가 보상은 일주일에 드물게 일어나 대부분은 세개의 보상만을 맏고 극히 드물게 추가보상을 받는다면, 이러한 확률때문에 lock을 하는 것은 낭비일 수 있다.
3) 이를 위해, 평상시에는 모두 보상에 접근하다가 특수한 경우 overwrite하여 보상을 덧붙이는 경우에만 lock을 한다. 이것을 RWLock or ReaderWriterLock
4) 아래의 코드는 ReaderWriterLock을 이용한 코드이다.
일반적인 보상을 받을때의 함수 GetReawardByld 함수를 보면 EnterReadLock()함수와 ExitReadLock()함수를 구현됨
-> WriteLock을 통해 특정 중요 스레드가 접근하지 않은 이상, 많은 스레드가 접근할 수 있음을 의미
AddReward 함수는 아주 특수하게 추가 보상이 나가서 보상을 덧붙이는 경우의 함수이다.
EnterWriteLock 함수와 ExitWriteLock 함수를 통해 구현
-> Write을 할 것이기 때문에 어떤 스레드도 접근하지 못함을 의미
class Reward
{
}
static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();
// 대게는 GetReward
static Reward GetRewardByld(int d)
{
_lock3.EnterReadLock();
_lock3.ExitReadLock();
return null;
}
// 드물게 AddReward
static void AddReward(Reward reward)
{
_lock3.EnterWriteLock();
_lock3.ExitWriteLock();
}
5. 대부분은 lock을 이용하여 우아하게 구현 가능하다.
<ReaderWriterLock 구현 연습>
실제로 ReaderWriteLock을 구현해보자!
1. 솔루션 탐색기 -> SeverCore에서 오른쪽 버튼을 눌러서 추가 -> 새항목 -> 클래스 -> Lock.cs 파일 만들기
2. Lock 클래스 내에 변수를 다음과 같이 설정한다.
1) 재귀적 락(스레드가 Acquire 후에 다시 Acquire을 할지)에 대한 것은 허용하지 않고
2) 스핀락 정책은 5000번 정도 기다린 후 Yield 하도록한다.
3) flag는 int형으로 32비트이다.
flag를 1, 15, 16비트 씩 나누어 다음과 같이 사용한다.
- Unused(1): 음수로 나올 경우에 대비해 비워둔다
- WriteThreadId: 스레드가 Write에 접근했을 시 해당 스레드의 Id 저장
- ReadCount: Read에 접근한 스레드의 개수
EMPTY_FLAG, WRITE_MASK, READ_MASK는 아래의 flag를 채우기 위한 비트를 16진수로 나타낸 것
* 32비트를 16진수로 표현할 때 4*8해서 8자리로 표현: 16진수 하나의 자리가 2진수 네자리 (순간 기억이 안났다...)
class Lock
{
const int EMPTY_FLAG = 0x00000000; // int 2^32 = 16^8
const int WRITE_MASK = 0x7FFF0000; // 0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// flag: [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
// - ReadCount: Read할때 여러 스레드 접근 가능, 이때 스레드의 개수
// - WriteThreadId: 어떤 스레드가 Write에 접근했는지 해당 스레드 Id
int _flag;
}
3. WriteLock, WriteUnlock, ReadLock, ReadUnlock을 구현해보자
1) WriteLock 함수
a) WriteLock 함수에서 행해야하는 것이 WriteLock과 ReadLock을 아무도 획득하지 않으면 소유권을 얻는 것
이때 SpinLock의 MAX count까지 flag를 확인한다. flag가 EMPTY_FLAG, 즉 아무도 획득하지 못했을때 해당 스레드가 소유권을 얻는다.
b) 소유권을 얻기위해서는 flag를 해당 ThreadId로 바꿔야한다.
Thread.CurrentThread.ManagedThreadId를 통해서 해당 스레드의 num(Id)값을 가져온 후 16비트 밀어준다. 후에 WRITE_MASK를 통해 중간의 15비트를 제외한 나머지 자리의 비트를 0으로 초기화한다.
해당 ThreadId를 desired에 넣고 _flag를 변경한다.
c) *문제 멀티스레드 환경에서 확인 후 넣어주는 코드를 원자적으로 해야함
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++)
{
// 시도를 해서 성공하면 return
//------ 문제------------
if (_flag == EMPTY_FLAG)
_flag = desired;
//-----------------------
}
Thread.Yield();
}
}
d) Interlocked.CompareExchange를 통해서(저번에 배운것과 같이) 한번에 비교하고 저장한다.
-> 동시에 두개의 스레드가 들어올 수 없다.
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++)
{
// 시도를 해서 성공하면 return
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
return;
}
Thread.Yield();
}
}
2) WriteUnlock 함수
: _flag를 EMPTY_FLAG로 바꾸면 됨(EMPTY_FLAG: 아무도 Read와 Write를 하지않음)
3) ReadLock 함수
: 아무도 WriteLock을 획득하고 있지 않으면, Readcount를 1 늘린다.
a) WriteLock 획득 여부 판단을 위해서는 중간의 15비트가 0인지 확인하면 된다. => _flag & WRITE_MASK
b) *문제 : 마찬가지로 아래의 if문이 문제 -> 만약 두개의 스레드가 0인지를 확인한 후 동시에 _flag를 +1 늘릴 수 있다.
public void ReadLock()
{
// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1늘린다.
while (true)
{
for(int i = 0; i < MAX_SPIN_COUNT; i++)
{
// -----문제-----------------
if((_flag & WRITE_MASK) == 0)
{
_flag = _flag + 1;
return;
}
//---------------------------
}
}
}
c) 이전처럼 Interlocked.CompareExchange를 사용하여 원자적으로 해결
-> 이때, expected에 _flag & READ_MASK를 통해 READ_MASK 이외의 부분을 0으로 초기화 한다. => ReadCount 부분만 남음
expected와 _flag를 비교했을 때, 같으면 WriteThreadId가 0인 것이므로 flag의 count을 1을 추가하는 Interlocked.CompareExchange 코드를 작성한다.
public void ReadLock()
{
// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1늘린다.
while (true)
{
for(int i = 0; i < MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK); // Write_Mask 부분을 날렸다.
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
if((_flag & WRITE_MASK) == 0)
{
_flag = _flag + 1;
return;
}
}
}
}
d) 위의 Interlock을 사용할 경우, 두개의 스레드가 ReadLock 함수에 접근하여도 더 먼저 권한을 획득한 스레드가 flag를 +1 한다. 권한을 얻지 못한 스레드가 flag와 expected를 비교하면 둘이 다르기 때문에 실패하고 다음 턴에 다시 접근한다.
4) ReadUnlock 함수
: Interlock.Decrement를 이용하여 flag의 값을 하나 줄인다.
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag);
}
* 아래는 전체 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
// 재귀적 락을 허용할지? : 같은 스레드에서 Acquire을 할때 허용할 것인지 -> No
// 스핀락 정책: 5000번 -> Yield
class Lock
{
const int EMPTY_FLAG = 0x00000000; // int 2^32 = 16^8
const int WRITE_MASK = 0x7FFF0000; // 0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// flag: [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
// - ReadCount: Read할때 여러 스레드 접근 가능, 이때 스레드의 개수
// - WriteThreadId: 어떤 스레드가 Write에 접근했는지 해당 스레드 Id
int _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++)
{
// 시도를 해서 성공하면 return
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
return;
}
Thread.Yield();
}
}
public void WriteUnLock()
{
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1늘린다.
while (true)
{
for(int i = 0; i < MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK); // Write_Mask 부분을 날렸다.
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
if((_flag & WRITE_MASK) == 0)
{
_flag = _flag + 1;
return;
}
}
}
}
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag);
}
}
}
4. 재귀적 lock을 허용
: Writelock을 잡고 있는 상태에서 Writelock을 잡고 있는 것, Writelock을 잡고 있는 상태에서 Readlock을 잡는 경우를 허용한다.
1) _writeCount 변수를 선언하여 현재 Writelock이 잡혀진 횟수를 센다.
2) WriteLock 함수에서 동일 스레드가 Writelock에 접근하고 있는지 확인한 후 동일하면 _writeCount의 값을 +1 한다.
-> 현재 쓰레드 Id는 _flag & WRITE_MASK를 통해 ThreadId를 가져오고 현재 스레드와 동일한지 확인
const int EMPTY_FLAG = 0x00000000; // int 2^32 = 16^8
const int WRITE_MASK = 0x7FFF0000; // 0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// flag: [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
// - ReadCount: Read할때 여러 스레드 접근 가능, 이때 스레드의 개수
// - WriteThreadId: 어떤 스레드가 Write에 접근했는지 해당 스레드 Id
int _flag;
int _writeCount = 0;
public void WriteLock()
{
// 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
_writeCount++;
return;
}
2) WriteUnlock 함수에서는 _writeCount를 1줄이고 해당 값이 0인 경우에만, lock을 해제한다.
public void WriteUnLock()
{
int lockCount = --_writeCount;
if(lockCount == 0)
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
3) 마찬가지로 ReadLock 함수에도 위의 WriteLock과 같은 코드를 위에 추가해야하지만, 이때는 flag의 값을 1을 추가야한다.
-> WriteLock을 잡은 상태에서 ReadLock을 잡는 것이기 1때문에
public void ReadLock()
{
// 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
Interlocked.Increment(ref _flag);
return;
}
4) 중요한 점은 WriteLock -> ReadLock을 하면 Unlock하는 순서는 ReadLock -> WriteLock을 해야한다.
* 전체 Lock.cs 코드
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
// 재귀적 락을 허용할지? : 같은 스레드에서 Acquire을 할때 허용할 것인지 -> No
// 스핀락 정책: 5000번 -> Yield
class Lock
{
const int EMPTY_FLAG = 0x00000000; // int 2^32 = 16^8
const int WRITE_MASK = 0x7FFF0000; // 0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// flag: [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
// - ReadCount: Read할때 여러 스레드 접근 가능, 이때 스레드의 개수
// - WriteThreadId: 어떤 스레드가 Write에 접근했는지 해당 스레드 Id
int _flag;
int _writeCount = 0;
public void WriteLock()
{
// 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
_writeCount++;
return;
}
// 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다.
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
while (true)
{
for(int i = 0; i < MAX_SPIN_COUNT; i++)
{
// 시도를 해서 성공하면 return
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
{
_writeCount = 1;
return;
}
}
Thread.Yield();
}
}
public void WriteUnLock()
{
int lockCount = --_writeCount;
if(lockCount == 0)
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
// 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
Interlocked.Increment(ref _flag);
return;
}
// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1늘린다.
while (true)
{
for(int i = 0; i < MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK); // Write_Mask 부분을 날렸다.
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
if((_flag & WRITE_MASK) == 0)
{
_flag = _flag + 1;
return;
}
}
}
}
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag);
}
}
}
5. Test
: main Program.cs 파일에 이전에 테스트 했던 것과 같이 두 개의 Task를 만들고 실행한다.
실행 결과 0값이 콘솔창에 뜨는 것을 확인할 수 있다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
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 < 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);
}
}
}
2) WriteLock을 중첩하여 사용하더라도 Unlock을 제대로 해준다면 0값이 나온다
-> 만일 Unlock을 덜 해주면 프로그램이 끝나지 않고 계속 실행된다.(_flag가 EMPTY가 되지 않기 때문에 )
3) ReadLock은 여러 스레드의 동시 접근을 허용하기 때문에 엉뚱한 값이 나온다.
<Thread Local Storage>
1. 하나의 게임(하나의 프로그램)에서 게임 로직, DB, 클라이언트 세션, 로그 등 다양한 세션이 존재
: 이러한 다양한 세션이 서로 연관되어 있기 때문에 이론적으로는 굉장히 많은 lock이 필요하다. (두개의 스레드가 하나에 동시에 접근하면 안됨)
1) 하나의 로직에 명령이 모일 경우 문제가 된다.
ex) MMORPG 게임에서 게임의 한 공간에 사용자들이 모이게 되면 클라이언트 세션의 많은 스레드가 하나의 공간에 패킷을 보내게 된다.하지만 Lock은 상호 배타적이기 때문에 한 번에는 하나의 스레드밖에 처리할 수 없다.
=> 멀티스레드 환경이라고 해서 무조건 lock을 거는 것이 최선의 방법은 아니다.
2. Thread Local Storage
: 하나의 스레드가 Heap 영역에서 처리해야할 데이터를 여러개 가져와 Thread Local Storage에 저장한 후 처리한다면, 하나씩 가져와 처리해야할 때보다 더 효율이 늘어난다.
-> 여러개의 스레드가 하나의 공간에 몰렸을때, 스레드마다 Heap 영역에서 하나씩 가져와서 처리하는 것보돠 Heap 영역에서 여러개 가져와 처리하는 것이 보다 효율적
1) ThreadLocal
ThreadLocal을 이용하여 각자 Thread마다 고유의 저장장소를 만든다.
-> 각자 고유의 저장 장소에 값이 저장되기 때문에 같은 함수로 Value를 바꾼다고 하여도 영향없이 없다
다음과 같이 WhoAmI 함수에서 ThreadName을 바꾼 후 Console창에 출력하는 코드를 짠 후
Thread 툴에서 여러개의 스레드를 뽑아 쓸 수 있는 Parallel.Invoke를 통해 WhoAmI를 실행하는 스레드 여러 개를 만들고 실행한다.
결과는 각자 다른 스레드 ID값이 출력된다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
class Program
{
// 스레드 자신만의 공간에 할당되기 때문에 수정하여도 다른 스레드에 영향 x
static ThreadLocal<string> ThreadName = new ThreadLocal<string>();
static void WhoAmI()
{
ThreadName.Value = $"My Name Is {Thread.CurrentThread.ManagedThreadId}";
Thread.Sleep(1000);
Console.WriteLine(ThreadName);
}
static void Main(string[] args)
{
Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI);
}
}
}
2) 그냥 static 변수
그냥 전역 변수로 ThreadName을 선언할 시, 계속 다른 스레드와 공유하다가 마지막에 ThreadName을 수정한 스레드에 의해 해당 값이 출력되기 때문에 다음과 같이 결과가 나온다.
3. 위의 코드의 문제
: 계속해서 스레드가 같은 Value를 덮어 쓰고 있음
1) 아래와 같이 람다를 사용하여 value를 바로 ThreadName에 넣을 수 있도록한다.
2) WhoAmI 함수에서 ThreadName.IsValueCreated를 통해 해당 Value가 이미 있는지의 여부를 확인할 수 있다.
만일 Value가 이미 있으면 repeat과 함께 출력하도록 한다.
class Program
{
// 스레드 자신만의 공간에 할당되기 때문에 수정하여도 다른 스레드에 영향 x
static ThreadLocal<string> ThreadName = new ThreadLocal<string>(() => { return $"My Name Is {Thread.CurrentThread.ManagedThreadId}"; });
static void WhoAmI()
{
bool repeat = ThreadName.IsValueCreated;
if (repeat)
Console.WriteLine(ThreadName.Value + "(repeat)");
else
Console.WriteLine(ThreadName.Value);
}
static void Main(string[] args)
{
Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI);
}
}
3) Console.WriteLine(ThreadName.Value); 에서 breakpoint를 잡으면 해당 Valu의 값이 null 일때 람다 함수를 실행한다는 것을 알 수 있음
4. Dispose를 통해 ThreadName의 값을 날려줄 수 있다.
ThreadName.Dispose() 함수로 날려줄 수 있음.
5. 응용
: Heap영역의 Queue에서 한번에 가져와서 Lock 없이 자유롭게 접근할 때 사용한다.
'Development > 게임 서버' 카테고리의 다른 글
[Part 4 게임 서버] 네트워크 프로그래밍 2 (Listener) (0) | 2022.01.16 |
---|---|
[Part 4 게임 서버] 네트워크 프로그래밍 1(네트워크 기초 이론 ~ 소켓 프로그래밍 입문 #2 (0) | 2022.01.14 |
[Part 4 게임 서버] 멀티쓰레드 프로그래밍 5(Lock 구현 이론 ~ AutoResetEvent) (0) | 2022.01.10 |
[Part 4 게임 서버] 멀티쓰레드 프로그래밍 4(Lock 기초, DeadLock) (0) | 2022.01.07 |
[Part 4 게임 서버] 멀티쓰레드 프로그래밍 3 (Interlocked) (0) | 2022.01.07 |