Read Write Lock은 스레드의 작업 중 읽기와 쓰기 작업을 나누어서 lock으로 관리하는 것이다. 만약 한 스레드가 작업을 위해 공유 자원에 접근하여 읽고 있는 중에 다른 스레드가 공유 자원의 값을 업데이트 해버리면 어떻게 될까? 읽고 있던 스레드는 작업을 위해 얻은 데이터의 일관성이 깨지게 된다. 이를 막기 위해서 lock으로 문제를 해결할 수 있지만, 한 번에 하나의 스레드만 접근하는 방식보단, 될 수 있으면 많은 스레드가 공유 자원의 일관성을 깨지 않고 작업을 진행하는 것이 훨씬 효율적이라고 생각하지 않는가? 이런 아이디어에서 나온 lock 방식이다.
Read Write Lock은 재귀적으로 작동할 수 있다. lock에 한 번 진입한(lock을 얻은) 스레드는 다른 lock에도 접근할 수 있다는 것이다. 모든 lock에 접근할 수 있는 것은 아니다. 예시로 들어보면 읽기 작업을 하기 위해 Read Lock을 얻은 후 다시 Read Lock을 얻을 수 있다. 그러나 Read Lock을 얻은 후에는 Write Lock을 얻을 수 없다. 이는 읽고 있는 락이 있으면서 쓰기 작업을 하게 된다면 데이터의 일관성이 깨지기 때문이다.
직접 구현하는 부분은 인프런의 [ C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 Part4: 게임 서버 ] 를 보며 작성하였다.
const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000;
const int READ_MASK = 0x0000FFFF;
위 플래그는 Read - Write lock에서 lock을 얻는 과정에서 비트 마스크 연산을 위해 사용할 상수이다. 우리는 _flag라는 변수를 따로 선언하여 lock을 구현해볼 것이다.
int _flag = EMPTY_FLAG;
int writeCount = 0;
void WriteLock() {
//같은 스레드가 재귀적으로 락에 접근했을 경우
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (Thread.CurrentThread.ManagerdId == lockThreadId) {
writeCount++;
return;
}
while (true) {
int desired = (Thread.CurrentThread.ManageredThreadId << 16) & WRITE_MASK;
// Write Lock을 얻기 위해서 현재 작동하는 스레드의 아이디를 Write 부분에 넣어준다.
for (int i = 0; i < 5000; i++) {
// 간단한 스핀락의 형태로 5000번 시도 후 못 얻었다면 양보한다.
// 만약 _flag의 상태가 비어있다면 lock을 얻는다.
if (InterLocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
return;
}
Thread.Yield();
}
}
void WriteUnlock() {
int lockCount = --writeCount;
if (lockCount == 0)
InterLocked.Exchange(ref _flag, EMPTY_FLAG);
}
Write Lock은 재귀적으로 작동할 수 없지만 Read Lock을 읽기만 하기 때문에 재귀적으로 작동할 수 있다. 따라서 카운트하는 변수를 두어 관리한다.
void ReadLock() {
int lockThreadId = (_flag & WRITE_MASK) >> 16;
if (lockThreadId == Thread.CurrentThread.ManagedThreadId) {
Interlocked.Increment(ref _flag);
return;
}
while (true) {
for (int i = 0; i < 5000; i++) {
int expected = _flag & READ_MASK;
if (Interlocked.CompareExchange(ref _flag, expected+1, expected) == expected)
return;
}
}
Thread.Yield();
}
void ReadUnlock() {
Interlocked.Decrement(ref _flag);
}
사용 방법은 읽는 작업 시 ReadLock 함수를 실행하고, 작업이 끝나면 ReadUnlock 함수를 호출하면 된다. 재귀적으로 실행하고 싶을 때는, ReadLock 함수를 2번 호출하였다면 ReadUnlock 함수를 2번 호출하면 된다.
WriteLock -> WriteLock, WriteLock -> ReadLock, ReadLock -> ReadLock은 가능하지만 ReadLock -> WriteLock은 동작하지 않는다.
'OS' 카테고리의 다른 글
OS - Lock 경쟁 / 구현 - 스핀락(Spin Lock), 양보(Yield), AutoResetEvent (0) | 2024.07.18 |
---|---|
OS - Context Switching (문맥 교환) (1) | 2024.07.16 |
OS - Lock, InterLocked - C# (0) | 2024.07.15 |
OS - 메모리 배리어 - Memory Barrier (0) | 2024.07.11 |
OS - 캐시(Cache) (0) | 2024.07.09 |