표준 mutex를 쓰지 않고 구현하여 쓰는 이유가 있다.
첫 번째, 표준 mutex는 재귀적으로 lock을 잡을 수 없다. 하지만 표준에서 제공하는 recursive_mutex를 사용하면 이 문제는 해결할 수 있다.
두 번째, 수정이 거의 일어나지 않는 공유 메모리에 mutex를 쓰면 하나의 쓰레드만 메모리를 읽을 수 있기 때문에 자원낭비가 심하다.
이번에 구현할 Read-Writer lock은 재귀적으로 lock잡을 수 있고, read일 경우 상호 배타적이지 않게 공유 메모리접근이 가능하다.
Spinlock으로 구현한다. 32bit 메모리를 활용하여 Write와 Read를 구분한다. 상위 16bit는 Write관련 정보 그 다음 16bit는 Read관련 정보를 저장한다.
[W][W][W][W][W][W][W][W] [W][W][W][W][W][W][W][W] [R][R][R][R][R][R][R][R] [R][R][R][R][R][R][R][R]
W에는 thread id를 저장한다. Write lock은 상호배타적이다. R에는 lock count를 저장한다. 어떤 한 쓰레드가 Write하고 있지 않으면 동시에 여러 쓰레드가 Read하는 것이 가능하다. (Write할 때 쓰레드가 lock을 소유, Read할 때는 쓰레드가 lock을 공유한다고 생각하자)
구현에 앞서 정책을 정한다.
동일 쓰레드 한해서, Write한 쓰레드가 Write lock 중, Read하는 것은 가능하다. 그러나 같은 쓰레드여도 Read한 쓰레드는 Read lock을 풀지 않는 한, Write하는 것은 불가능하다.
먼저 Custom Lock클래스를 정의한다.
Lock.h
class Lock
{
enum : uint32
{
ACQUIRE_TIMEOUT_TICK = 10000,
MAX_SPIN_COUNT = 5000,
WRITE_THREAD_MASK = 0xFFFF'0000,
READ_COUNT_MASK = 0x0000'FFFF,
EMPTY_FLAG = 0x0000'0000
};
public:
void WriteLock();
void WriteUnlock();
void ReadLock();
void ReadUnlock();
private:
Atomic<uint32> _lockFlag = EMPTY_FLAG;
uint16 _writeCount = 0;
};
16진수 8자리로 Write-Read lock flag를 저장할 수 있다.
Lock.cpp
#include "pch.h"
#include "Lock.h"
void Lock::WriteLock()
{
// 동일한 쓰레드가 소유하고 있다면 무조건 성공.
const uint32 lockThreadId = (_lockFlag.load() & WRITE_THREAD_MASK) >> 16;
if (LThreadId == lockThreadId)
{
_writeCount++;
return;
}
// 아무도 소유 및 공유하고 있지 않을 때, 경합해서 소유권을 얻는다.
const int64 beginTick = ::GetTickCount64();
const uint32 desired = ((LThreadId << 16) & WRITE_THREAD_MASK);
while (true)
{
for (uint32 spinCount = 0; spinCount < MAX_SPIN_COUNT; spinCount++)
{
uint32 expected = EMPTY_FLAG;
if (_lockFlag.compare_exchange_strong(OUT expected, desired))
{
_writeCount++;
return;
}
}
if (::GetTickCount64() - beginTick >= ACQUIRE_TIMEOUT_TICK)
CRASH("LOCK_TIMEOUT");
this_thread::yield();
}
}
void Lock::WriteUnlock()
{
// ReadLock 다 풀기 전에는 WriteUnlock 불가능
if ((_lockFlag.load() & READ_COUNT_MASK) != 0)
CRASH("INVALID_UNLOCK_ORDER");
const int32 lockCount = --_writeCount;
if (lockCount == 0)
_lockFlag.store(EMPTY_FLAG);
}
void Lock::ReadLock()
{
// 동일한 쓰레드가 소유하고 있다면 무조건 성공.
const uint32 lockThreadId = (_lockFlag.load() & WRITE_THREAD_MASK) >> 16;
if (LThreadId == lockThreadId)
{
_lockFlag.fetch_add(1);
return;
}
// 아무도 소유하고 있지 않을 때, 경합해서 공유 카운트를 올린다.
const int64 beginTick = ::GetTickCount64();
while(true)
{
for (uint32 spinCount = 0; spinCount < MAX_SPIN_COUNT; spinCount++)
{
uint32 expected = (_lockFlag.load() & READ_COUNT_MASK);
if (_lockFlag.compare_exchange_strong(OUT expected, expected + 1))
return;
}
if (::GetTickCount64() - beginTick >= ACQUIRE_TIMEOUT_TICK)
CRASH("LOCK_TIMEOUT");
this_thread::yield();
}
}
void Lock::ReadUnlock()
{
if ((_lockFlag.fetch_sub(1) & READ_COUNT_MASK) == 0)
CRASH("MULTIPLE_UNLOCK");
}
RAII에 따라 사용할 수 있도록 LockGuard클래스도 만든다.
Lock.h
/*--------------------------
LockGuards
--------------------------*/
class ReadLockGuard
{
public:
ReadLockGuard(Lock& lock) : _lock(lock) { _lock.ReadLock(); }
~ReadLockGuard() { _lock.ReadUnlock(); }
private:
Lock& _lock;
};
class WriteLockGuard
{
public:
WriteLockGuard(Lock& lock) : _lock(lock) { _lock.WriteLock(); }
~WriteLockGuard() { _lock.WriteUnlock(); }
private:
Lock& _lock;
};
Lock관련 선언, 함수를 매크로로 정의하면 사용하기 편하다.
CoreMacro.h
/*----------------------
Lock
----------------------*/
#define USE_MANY_LOCKS(count) Lock _locks[count];
#define USE_LOCK USE_MANY_LOCKS(1)
#define READ_LOCK_IDX(idx) ReadLockGuard readLockGuard_##idx(_locks[idx]);
#define READ_LOCK READ_LOCK_IDX(0)
#define WRITE_LOCK_IDX(idx) WriteLockGuard writeLockGuard_##idx(_locks[idx]);
#define WRITE_LOCK WRITE_LOCK_IDX(0)
만든 코드를 테스트해보자
class TestLock
{
USE_LOCK;
public:
int32 TestRead()
{
READ_LOCK;
if (_queue.empty())
return -1;
return _queue.front();
}
void TestPush()
{
WRITE_LOCK;
_queue.push(rand() % 100);
}
void TestPop()
{
WRITE_LOCK;
if (_queue.empty() == false)
_queue.pop();
}
private:
queue<int32> _queue;
};
TestLock testLock;
void ThreadWrite()
{
while (true)
{
testLock.TestPush();
this_thread::sleep_for(1ms);
testLock.TestPop();
}
}
void ThreadRead()
{
while (true)
{
int32 value = testLock.TestRead();
cout << value << endl;
this_thread::sleep_for(1ms);
}
}
int main()
{
for (int32 i = 0; i < 2; i++)
{
GThreadManager->Launch(ThreadWrite);
}
for (int32 i = 0; i < 5; i++)
{
GThreadManager->Launch(ThreadRead);
}
GThreadManager->Join();
}
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 대시보드 - 인프런 | 강의 (inflearn.com)
Reference Counting (0) | 2021.09.08 |
---|---|
Deadlock 탐지 (0) | 2021.09.06 |
ThreadManager (0) | 2021.09.01 |
Lock free stack #3 (0) | 2021.08.31 |
Lock free stack #2 (0) | 2021.08.26 |
댓글 영역