기록은 기억을 이기고 시간보다 오래 남는다.

게임 서버

Reader-Writer Lock

준_준 2024. 5. 29. 14:46

안녕하세요, 준준입니다.

 

이번 게시물에서는 **RwLock(Read-Write Lock)**에 대해 알아보겠습니다.

 

RwLock은 읽기쓰기의 동시 접근을 관리하기 위한 동기화 메커니즘입니다.

여러 스레드가 데이터를 읽을 때는 동시에 접근할 수 있지만,

데이터를 쓸 때는 오직 하나의 스레드만이 접근할 수 있도록 하는 기능을 제공합니다.

아래의 그림을 보면 이해가 쉬우실까요?

RwLock 예시 사진

 

왜 필요한가요?


 

일반적으로 데이터는 읽기가 빈번하게 일어나고

쓰기는 그다지 자주 발생하지 않는 경우가 많습니다.

 

매번 Lock을 걸어둔다면 쓰기 작업이 거의 없는 상황에서도

불필요한 경합(Contension)이 발생할 수 있습니다.

 

RwLock은 이러한 상황에서 효율적으로 동작하여 성능을 향상할 수 있습니다.

 


 

RwLock의 주요 특징은 다음과 같습니다:


읽기 모드
(Read Mode)
여러 스레드가 동시에 읽기 잠금을 얻을 수 있습니다.
읽기 중에는 데이터가 변경되지 않기 때문에 여러 스레드가 동시에 읽어도 문제가 발생하지 않습니다.
쓰기 모드
(Write Mode):
 오직 하나의 스레드만이 쓰기 잠금을 얻을 수 있습니다.
이때 읽기와 쓰기가 동시에 일어나지 않도록 합니다. 다수의 읽기 스레드가 동시에 쓰기 작업이 진행 중일 때는 기다려야 합니다.
읽기와 쓰기 간교차 옵션 몇몇 RwLock 구현은 읽기와 쓰기 간의 교차를 허용하는 옵션을 제공할 수 있습니다.
이는 쓰기 작업이 진행 중에도 읽기를 허용하거나, 읽기 작업이 진행 중에도 쓰기를 허용하는 등의 설정을 의미합니다.

 

RwLock은 주로 데이터베이스, 캐시, 파일 시스템 등의 공유 자원에 대한 접근을 관리하는 데 사용됩니다.

이를 통해 동시성을 향상시키고 성능을 최적화할 수 있습니다.

다만, 쓰기 작업이 빈번하게 발생하는 경우에는 여전히 경합 조건 등의 문제에 주의해야 합니다.

 

이렇게 사용되는 이유와 주요 특징을 고려하여 RwLock을 적재적소에 활용하면 좋습니다.

계속해서 유용한 정보를 제공하도록 노력하겠습니다! 

감사합니다.

 


 

아래는 관련 코드이고 헤더 파일 입니다.

#pragma once
/*
RW SpinLock
*/
/*
[\\\\\\\\][\\\\\\\\][RRRRRRRR][RRRRRRRR]
W : WriteFlag (Excluxive Lock Owner ThreadId)
R : ReadFlag(Shared Lock Count)
*/

//W->W(0)
//W->R(0)
//R->W(X)

class Lock
{
	enum : uint32 {
		ACQUIRE_TIMEOUT_TICK=10000, //최대로 기다려줄 틱 설정
		MAX_SPIN_COUNT = 5000,		//스핀 카운트를 최대 몇번 돌것인가
		WRITE_THREAD_MASK =0xFFFF'0000,	//상위 16비트만 뽑아 오기 위함
		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; //Lock을 잡은 스레드가 독단적으로 사용

};

/*
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;
};

 

아래는 cpp파일입니다.

#include "pch.h"
#include "Lock.h"
#include "CoreTLS.h"

void Lock::WrteLock()
{

	//동일한 쓰레드가 소유하고 있다면 무조건 성공
	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(); //sleep과 비슷하나, sleep이 쓰레드를 일시 정지 상태로						바꾸는 반면, yield는 계속 실행 대기 상태를 유지합니다.
	}

}

void Lock::WriteUnlock()
{
	//ReadLock 다 풀기 전에는 WriteUnlock 불가능
	if ((_lockFlag.load() & READ_COUNT_MASK) != 0)
		CRASH("INVALIDID_UNLOCK_ORDER");
		
	const int32 lockCount = --_writeCount;
	if (lockCount == 0) //더이상 LOCK을 잡고 있지 않다
		_lockFlag.store(EMPTY_FLAG); //THREAD ID를 다시 0으로
}

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();// max spin count 만큼 시도 하였는데도 불구하고 실패해서 잠시 쉼
}

void Lock::ReadUnlock()
{
	if ((_lockFlag.fetch_sub(1) & READ_COUNT_MASK) == 0);
		CRASH("MULTIPLE_UNLOCK");
}

 

추가적으로 

//W->W(0)
//W->R(0)
//R->W(X)

 

 

반응형

'게임 서버' 카테고리의 다른 글

TLS(Thread Local Storage) with c++  (0) 2024.05.29
게임 회사 채용 준비과정  (0) 2023.02.22
게임 서버 개발자 채용 프로세스  (0) 2023.02.14
게임 서버 공부 할 것  (0) 2023.02.05