반응형
C# 내장 SpinLock
static object _lock = new object();
static void Main(string[] args)
{
bool lockTaken = false;
try
{
_lock2.Enter(ref lockTaken);
}
finally
{
if (lockTaken) _lock2.Exit();
}
}
//내부적으로 무한 뺑뺑이가 아닌, 너무 오래걸린다면 양보를 하도록 설계됨
- 이전에 직접 구현해 본 spinlock은 c#에 자체적으로 위와 같이 존재함
- 어떤 락이든, 기본 철학은 상호 배제
ReaderWriterLock
- 읽기는 락을 안걸고, 쓸 때만 락을 걸고 싶다면?
- 즉, 평소에는 락이 필요 없는데 특수한 경우에만 락이 필요한 경우
class Reward
{
}
//선언 형식
static ReaderWriterLockSlim _lock3 = new ReaderWriterLockSlim();
//자주 호출되는 함수
static Reward GetRewardById(int id)
{
_lock3.EnterReadLock();
//여러 쓰레드가 동시 접근은 가능하지만, 쓰기 작업은 차단
_lock3.ExitReadLock();
return null;
}
//일주일에 한번 호출되는 함수
static void AddReward(Reward reward)
{
_lock3.EnterWriteLock();
//단 하나의 쓰레드만 접근 가능, 쓰기 가능
_lock3.ExitWriteLock();
}
- 이렇게 Read와 Write의 락을 구분하여 각 용도에 맞게 사용
- EnterReadLock() : 여러 쓰레드의 접근은 허용하지만, 쓰기 작업은 차단
- EnterWriteLock() : 하나의 쓰레드만 접근 허용하며, 쓰기 작업도 가능
ReaderWriterLock 직접 구현해보기(SpinLock 방식으로)
- 재귀적 락을 허용하지 않는 버전 ↓
// 재귀적 락을 허용할지 (No)
//락을 얻은 쓰레드에서 한번 더 락을 얻는 것을 허용할 것인지
// 스핀락 정책 (5000번 -> Yield)
class Lock
{
const int EMPTY_FLAG = 0x00000000; //첫 1비트 // 0000 0000 0000 0000 0000 0000 0000 0000
const int WRITE_MASK = 0x7FFF000; //두번째부터 15비트 //0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; //뒤 16비트 // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// [Unused(1bit)] [WriteThreadId(15bit)] [ReadCount(16bit)]
int _flag = EMPTY_FLAG;
public void WriteLock()
{
//ThreadID를 가져와서, 15비트를 저장하는 방법
//16비트를 밀고 WRITE_MASK와 & 연산을 통해 범위를 넘어가지 않도록 설정
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
// 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 소유권을 경합해서 얻는다
while(true)
{
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
//시도를 해서 성공하면 return
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
return;
//여러개가 동시에 들어와도 ID가 다르기 때문에 누군가 승자는 존재함
/*if(_flag == EMPTY_FLAG)
{
_flag = desired;
return;
}*/
}
Thread.Yield(); //5000번 하고 실패시 양보
}
}
public void WriteUnlock()
{
//저장된 Thread ID를 밀어버리면 된다
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
// 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1 늘린다
// 여러 쓰레드가 동시에 Read를 잡을 수 있게 하기 위함
while(true)
{
for(int i=0; i<MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK); //WRITE 부분을 0으로 밀어버린 값이 기대값
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
//A, B가 동시에 입장 했을 때
//A가 승자라고 하면, ReadCount를 0 -> 1로 바꿈
//B가 요청하려 할 때, expected가 0이어야 하는데, 실제 값은 1이 나옴
//for문 다음 반복으로 돌림
//돌리면 expected가 1이 됨, 이제 맞으므로 1 -> 2 연산을 수행함
/*if((_flag & WRITE_MASK) == 0) //& 연산하고 0이라면, 아무도 WRITE를 가지고 있지 않다는 뜻
{
_flag = _flag + 1;
return;
}*/
}
Thread.Yield();
}
}
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag); //Read Count 1 줄이기
}
}
- 재귀적 락을 허용하는 버전 ↓
// 재귀적 락을 허용
// WriteLock -> WriteLock, WriteLock -> ReadLock (o)
//ReadLock -> WriteLock (x)
class Lock
{
const int EMPTY_FLAG = 0x00000000; //첫 1비트 // 0000 0000 0000 0000 0000 0000 0000 0000
const int WRITE_MASK = 0x7FFF000; //두번째부터 15비트 //0111 1111 1111 1111 0000 0000 0000 0000
const int READ_MASK = 0x0000FFFF; //뒤 16비트 // 0000 0000 0000 0000 1111 1111 1111 1111
const int MAX_SPIN_COUNT = 5000;
// [Unused(1bit)] [WriteThreadId(15bit)] [ReadCount(16bit)]
int _flag = EMPTY_FLAG;
int _writeCount = 0; //재귀 write를 위한 카운터 추가
public void WriteLock()
{
//동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
_writeCount++; //이미 갖고 있다면 count만 올려주면 끝
return;
}
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
while(true)
{
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
{
_writeCount = 1;
return;
}
}
Thread.Yield();
}
}
public void WriteUnlock()
{
//락 카운트를 unlock 할 때마다 1씩 줄이다가
int lockCount = --_writeCount;
if (lockCount == 0) //그게 마지막 락이었다면, Release
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if(Thread.CurrentThread.ManagedThreadId == lockThreadId)
{
Interlocked.Increment(ref _flag); //리드카운트 늘리기
return;
}
while(true)
{
for(int i=0; i<MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK);
if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
return;
}
Thread.Yield();
}
}
public void ReadUnlock()
{
Interlocked.Decrement(ref _flag); //Read Count 1 줄이기
}
}
- 이 방식을 사용 할 때에 주의할 점!
- Write -> Read순서로 Lock을 얻었다면, Read -> Write 순서로 Unlock 해야 함
구현한 ReaderWriterLock 테스트
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);
}
반응형
'C# > 네트워크' 카테고리의 다른 글
[c#][서버] 네트워크 기초 이론 (0) | 2024.06.04 |
---|---|
[c#][서버] Thread Local Storage (0) | 2024.06.03 |
[c#][서버] Lock 구현 이론 (0) | 2024.06.02 |
[c#][서버] 데드락 (0) | 2024.06.02 |
[C#][서버] Lock 기초 (0) | 2024.06.02 |