📄 ReaderWriterLock
ReaderWriterLock은 이전 글[유니티 서버 구축(C#) 9.ReaderWriterLock]에서 다뤘습니다. 상호배제를 위한 기능은 동일하지만, 데이터에 대해 읽기, 쓰기에 대한 호출의 비율이 매우 편중되어있을 때 적합한 방법입니다.
📑 Lock.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
//재귀적 락을 허용하지 않는 모델
class Lock
{
//음수가 될 수 있기에 최상위비트 사용하지 않음
const int EMPTY_FLAG = 0x00000000;
//마스크를 사용하여 필요한 영역만 빠르게 추출하기 위해 사용
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
const int READ_MASK = 0x0000FFFF; //16자리
//스핀락 정책: 5000번 시도 후 yield
const int MAX_SPIN_COUNT = 5000;
//int형 비트를 다음과 같이 사용: [Unused(1)] [WriteThreadID(15)] [ReadCount(16)]
//WriteThreadID는 쓰기작업을 하여 Lock을 하는 스레드ID
int mFlag = EMPTY_FLAG;
int mWriteCount = 0;
/// <summary>
/// 아무도 WriteLock 또는 ReadLock을 획득하고 있지 않을때 경합해서 소유권을 획득
/// </summary>
public void WriteLock()
{
//동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득
//스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
{
++mWriteCount;
return;
}
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; ++i)
{
//스레드 ID를 얻어 16비트만큼 Left-Shift하여 위치시키고
//WRITE_MASK와 AND연산으로 다른 영역의 비트를 모두 0으로 한 후에 mFlag에 삽입
if (Interlocked.CompareExchange(ref mFlag, (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK, EMPTY_FLAG) == EMPTY_FLAG)
{
mWriteCount = 1;
return;
}
}
//5000번 시도 후 소유권 획득 실패시 yield
Thread.Yield();
}
}
public void WriteUnlock()
{
int lockCount = --mWriteCount;
//Write락을 언락할때, 락카운트가 0이 되는 경우에만 락해제 가능
if (lockCount == 0) { Interlocked.Exchange(ref mFlag, EMPTY_FLAG); }
}
/// <summary>
/// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1올린다.
/// </summary>
public void ReadLock()
{
//동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득
//스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
{
++mFlag;
return;
}
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; ++i)
{
int expected = mFlag & READ_MASK;
//만약 mFlag가 expected라면, 아무도 WriteLock을 사용하고 있지 않음(WRITE_MASK는 0)
//expected가 1을 늘리는 도중(expected + 1)에 어딘가에서 1을 이미 올렸다면 expected가 아닐 수 있음
if (Interlocked.CompareExchange(ref mFlag, expected + 1, expected) == expected) { return; }
}
//5000번 시도 후 소유권 획득 실패시 yield
Thread.Yield();
}
}
public void ReadUnlock()
{
//Read가 끝나 Unlock을 할때 1을 감소하도록 함
Interlocked.Decrement(ref mFlag);
}
}
}
- ReaderWriterLock을 구현한 Lock 클래스입니다.
- 재귀적 락을 허용하는 모델입니다.
const int EMPTY_FLAG = 0x00000000; //음수가 될 수 있기에 최상위비트 사용하지 않음
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
const int READ_MASK = 0x0000FFFF; //16자리
int mFlag = EMPTY_FLAG; //사용할 플래그 정수(비트로 연산)
- mFlag를 이용하여 락에 대한 연산을 수행합니다.
- 정수 값이 아닌, 내부의 비트(32 bits)를 이용하여 계산합니다.
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
- WRITE_MASK는 WriterLock을 연산하기위한 마스크입니다.
- 총 15자리를 사용하며 최상위비트를 제외합니다. signed integer에서 최상위비트가 1이면 음수로 바뀌면서 비트가 변하기 때문에 이를 방지하고자 하나의 비트를 제외합니다.
const int READ_MASK = 0x0000FFFF; //16자리
- READ_MASK는 ReaderLock을 연산하기위한 마스크입니다.
const int MAX_SPIN_COUNT = 5000;
- 스핀락 사용하여 5000번의 획득시도 중 실패할경우, yield 하도록 구현합니다.
public void WriteLock()
- 재귀적 락을 허용하기위해 먼저 lockThreadID를 구합니다. WRITE_MASK에서 나온 값을 ID와 비교하기 위해 RShift를 하여 비트위치를 맞춥니다.
- if (Thread.CurrentThread.ManagedThreadId == lockThreadID)가 참이라면. 락을 하고 있는 스레드가 또다시 WriteLock을 시도하는 것으로 mWriteCount를 1 증가시켜 줍니다.
- 다른 스레드라면, 5000번의 시도를 하여 mFlag가 EMPTY_FLAG인지 확인합니다.
- 만약 EMPTY_FLAG라면 아무도 Lock을 사용하지 않기 때문에 자신의 스레드 ID로 mFlag의 값을 바꿔줍니다.
public void WriteUnlock()
- WriteLock을 해제합니다.
- 재귀 락에 대응하기 위해 mWriteCount를 1씩 감소시켜 lockCount(감소된 Lock 수)가 0이라면 mFlag를 EMPTY_FLAG로 바꿔줍니다.
public void ReadLock()
- ReadLock을 합니다.
- 이미 동일한 스레드가 락을 하고 있는 경우에, 이미 락을 획득한 스레드이기 때문에 ++mFlag를 합니다.
- 5000번의 시도를 하여 락을 획득하고자 합니다. mFlag & READ_MASK가 mFlag와 같다면, 현재 WriteLock은 없는 상태이기에 ReadLock을 획득할 수 있습니다.
public void ReadUnlock()
- ReadLock을 해제합니다.
📑 Main.cs
namespace ServerCore
{
class Program
{
static volatile int count = 0;
static Lock _lock = new Lock();
private static void Main(string[] args)
{
Task t1 = new Task(delegate ()
{
for (int i = 0; i < 10000000; ++i)
{
_lock.WriteLock();
_lock.WriteLock();
++count;
_lock.WriteUnlock();
_lock.WriteUnlock();
}
});
Task t2 = new Task(delegate ()
{
for (int i = 0; i < 10000000; ++i)
{
_lock.WriteLock();
--count;
_lock.WriteUnlock();
}
});
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(count);
}
}
}
- 두 개의 태스크를 생성하여 두개의 스레드에서 +, - 연산을 하여 0이 나오는지 확인해 봅니다.

'server > socket server' 카테고리의 다른 글
| [C# 서버] 소켓 프로그래밍 (0) | 2023.01.14 |
|---|---|
| [C# 서버] Thread Local Storage (0) | 2023.01.12 |
| [C# 서버] ReaderWriterLock (0) | 2023.01.10 |
| [C# 서버] Mutex (0) | 2023.01.09 |
| [C# 서버] AutoResetEvent (1) | 2023.01.06 |
📄 ReaderWriterLock
ReaderWriterLock은 이전 글[유니티 서버 구축(C#) 9.ReaderWriterLock]에서 다뤘습니다. 상호배제를 위한 기능은 동일하지만, 데이터에 대해 읽기, 쓰기에 대한 호출의 비율이 매우 편중되어있을 때 적합한 방법입니다.
📑 Lock.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
//재귀적 락을 허용하지 않는 모델
class Lock
{
//음수가 될 수 있기에 최상위비트 사용하지 않음
const int EMPTY_FLAG = 0x00000000;
//마스크를 사용하여 필요한 영역만 빠르게 추출하기 위해 사용
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
const int READ_MASK = 0x0000FFFF; //16자리
//스핀락 정책: 5000번 시도 후 yield
const int MAX_SPIN_COUNT = 5000;
//int형 비트를 다음과 같이 사용: [Unused(1)] [WriteThreadID(15)] [ReadCount(16)]
//WriteThreadID는 쓰기작업을 하여 Lock을 하는 스레드ID
int mFlag = EMPTY_FLAG;
int mWriteCount = 0;
/// <summary>
/// 아무도 WriteLock 또는 ReadLock을 획득하고 있지 않을때 경합해서 소유권을 획득
/// </summary>
public void WriteLock()
{
//동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득
//스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
{
++mWriteCount;
return;
}
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; ++i)
{
//스레드 ID를 얻어 16비트만큼 Left-Shift하여 위치시키고
//WRITE_MASK와 AND연산으로 다른 영역의 비트를 모두 0으로 한 후에 mFlag에 삽입
if (Interlocked.CompareExchange(ref mFlag, (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK, EMPTY_FLAG) == EMPTY_FLAG)
{
mWriteCount = 1;
return;
}
}
//5000번 시도 후 소유권 획득 실패시 yield
Thread.Yield();
}
}
public void WriteUnlock()
{
int lockCount = --mWriteCount;
//Write락을 언락할때, 락카운트가 0이 되는 경우에만 락해제 가능
if (lockCount == 0) { Interlocked.Exchange(ref mFlag, EMPTY_FLAG); }
}
/// <summary>
/// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1올린다.
/// </summary>
public void ReadLock()
{
//동일한 스레드가 WriteLock을 이미 획득하고있는지 확인
int lockThreadID = (mFlag & WRITE_MASK) >> 16; //현재 플래그에서 스레드 ID획득
//스레드 ID가 동일한경우?, 이미 락을 획득한 스레드임
if (Thread.CurrentThread.ManagedThreadId == lockThreadID)
{
++mFlag;
return;
}
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; ++i)
{
int expected = mFlag & READ_MASK;
//만약 mFlag가 expected라면, 아무도 WriteLock을 사용하고 있지 않음(WRITE_MASK는 0)
//expected가 1을 늘리는 도중(expected + 1)에 어딘가에서 1을 이미 올렸다면 expected가 아닐 수 있음
if (Interlocked.CompareExchange(ref mFlag, expected + 1, expected) == expected) { return; }
}
//5000번 시도 후 소유권 획득 실패시 yield
Thread.Yield();
}
}
public void ReadUnlock()
{
//Read가 끝나 Unlock을 할때 1을 감소하도록 함
Interlocked.Decrement(ref mFlag);
}
}
}
- ReaderWriterLock을 구현한 Lock 클래스입니다.
- 재귀적 락을 허용하는 모델입니다.
const int EMPTY_FLAG = 0x00000000; //음수가 될 수 있기에 최상위비트 사용하지 않음
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
const int READ_MASK = 0x0000FFFF; //16자리
int mFlag = EMPTY_FLAG; //사용할 플래그 정수(비트로 연산)
- mFlag를 이용하여 락에 대한 연산을 수행합니다.
- 정수 값이 아닌, 내부의 비트(32 bits)를 이용하여 계산합니다.
const int WRITE_MASK = 0x7FFF0000; //최상위비트를 제외한 15자리
- WRITE_MASK는 WriterLock을 연산하기위한 마스크입니다.
- 총 15자리를 사용하며 최상위비트를 제외합니다. signed integer에서 최상위비트가 1이면 음수로 바뀌면서 비트가 변하기 때문에 이를 방지하고자 하나의 비트를 제외합니다.
const int READ_MASK = 0x0000FFFF; //16자리
- READ_MASK는 ReaderLock을 연산하기위한 마스크입니다.
const int MAX_SPIN_COUNT = 5000;
- 스핀락 사용하여 5000번의 획득시도 중 실패할경우, yield 하도록 구현합니다.
public void WriteLock()
- 재귀적 락을 허용하기위해 먼저 lockThreadID를 구합니다. WRITE_MASK에서 나온 값을 ID와 비교하기 위해 RShift를 하여 비트위치를 맞춥니다.
- if (Thread.CurrentThread.ManagedThreadId == lockThreadID)가 참이라면. 락을 하고 있는 스레드가 또다시 WriteLock을 시도하는 것으로 mWriteCount를 1 증가시켜 줍니다.
- 다른 스레드라면, 5000번의 시도를 하여 mFlag가 EMPTY_FLAG인지 확인합니다.
- 만약 EMPTY_FLAG라면 아무도 Lock을 사용하지 않기 때문에 자신의 스레드 ID로 mFlag의 값을 바꿔줍니다.
public void WriteUnlock()
- WriteLock을 해제합니다.
- 재귀 락에 대응하기 위해 mWriteCount를 1씩 감소시켜 lockCount(감소된 Lock 수)가 0이라면 mFlag를 EMPTY_FLAG로 바꿔줍니다.
public void ReadLock()
- ReadLock을 합니다.
- 이미 동일한 스레드가 락을 하고 있는 경우에, 이미 락을 획득한 스레드이기 때문에 ++mFlag를 합니다.
- 5000번의 시도를 하여 락을 획득하고자 합니다. mFlag & READ_MASK가 mFlag와 같다면, 현재 WriteLock은 없는 상태이기에 ReadLock을 획득할 수 있습니다.
public void ReadUnlock()
- ReadLock을 해제합니다.
📑 Main.cs
namespace ServerCore
{
class Program
{
static volatile int count = 0;
static Lock _lock = new Lock();
private static void Main(string[] args)
{
Task t1 = new Task(delegate ()
{
for (int i = 0; i < 10000000; ++i)
{
_lock.WriteLock();
_lock.WriteLock();
++count;
_lock.WriteUnlock();
_lock.WriteUnlock();
}
});
Task t2 = new Task(delegate ()
{
for (int i = 0; i < 10000000; ++i)
{
_lock.WriteLock();
--count;
_lock.WriteUnlock();
}
});
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(count);
}
}
}
- 두 개의 태스크를 생성하여 두개의 스레드에서 +, - 연산을 하여 0이 나오는지 확인해 봅니다.

'server > socket server' 카테고리의 다른 글
| [C# 서버] 소켓 프로그래밍 (0) | 2023.01.14 |
|---|---|
| [C# 서버] Thread Local Storage (0) | 2023.01.12 |
| [C# 서버] ReaderWriterLock (0) | 2023.01.10 |
| [C# 서버] Mutex (0) | 2023.01.09 |
| [C# 서버] AutoResetEvent (1) | 2023.01.06 |