본문 바로가기
MultiThread

Reader Writer Lock 구현

by W00gie 2021. 12. 13.

지금까지 공부한 Lock은 모든 작업에 대해 배타적인(한 스레드가 사용중에는 접근을 허용하지 않는) 사용방식을 사용했다. 하지만 다중 스레드 환경에서는 쓰기작업(Write)이 이루어지는 동안 읽기(Read)작업을 허용해주어야 하는 경우가 있다. 또한 동일한 스레드가 recursive lock을 수행하는 경우에도 동일한 스레드에 대해 잠궜던 영역에 대해 접근을 허용해 주어야한다.

이를 가능케 하는 Reader Writer Lock에 대해 정리해본다.

Reader Writer Lock을 구현하는데에 있어서 가장 중요한 부분은 멀티 쓰레드 환경에서 Reader 스레드와 Writer 스레드를 구분하는 일이다. 해당 Lock 구현에서는 이를 해결하기 위해 lock flag를 활용한다.

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

_lockFlag의 32비트 중 상위 16비트는 사용중인 Writer의 ThreadID를 기록하는데 사용하고, 하위 16비트는 접근한 Reader 의 갯수를 Count한다.  이후 해당 Flag에 Mask로 &연산을 사용하면 사용목적에 따른 비트만 꺼내쓸 수 있다.

WrtieLock

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();
	}
}

 

ReadLock

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();
	}
}

 

unlock의 경우 크게 어려울 것 없이 writeCount를 경감해주는 일을 신경써주면된다.

물론 Writer를 unlock하는 경우엔 다시 flag를 empty 상태로 되돌려주어야 다른 writer가 접근가능하다.