상세 컨텐츠

본문 제목

Lock free stack #3

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 8. 31. 22:05

본문

쓰지 않는 포인터를 모아 놨다가 한 번에 소멸시키는 로직을 shared_ptr ref count를 이용해 더 이상 참조하는 곳이 없으면 메모리 free하는 로직으로 바꿔보자.

 

template<typename T>
class LockFreeStack
{
	struct Node
	{
		Node(const T& value) : data(make_shared<T>(value)), next(nullptr)
		{

		}

		shared_ptr<T> data;
		shared_ptr<Node> next;
	};

public:
	void Push(const T& value)
	{
		shared_ptr<Node> node = make_shared<Node>(value);
		node->next = std::atomic_load(&_head);
		while (std::atomic_compare_exchange_weak(&_head, &node->next, node) == false)
		{

		}

	}

	shared_ptr<T> TryPop()
	{
		shared_ptr<Node> oldHead = std::atomic_load(&_head); // 원자적으로 ref count 증가

		while (oldHead && std::atomic_compare_exchange_weak(&_head, &oldHead, oldHead->next) == false)
		{

		}

		if (oldHead == nullptr)
			return shared_ptr<T>();

		return oldHead->data;
	}

private:
	shared_ptr<Node> _head;
	
};

 

 

하지만 shared_ptr을 원자적으로 바꾸는 동작이 lock없는 방식인지는 확인해보아야 한다.

 

shared_ptr<int32> ptr;
bool value = std::atomic_is_lock_free(&ptr);

 

cpu마다 다르지만 내 컴퓨터에서는 false가 나온다. lock을 사용하여 원자적동작을 구현했다는 의미다.

때문에 직접 ref count를 구현하여 사용하여야 한다.

 

아래 코드는 CountedNodePtr이 원자적으로 동작한다는 전제가 있다.

template<typename T>
class LockFreeStack
{
	struct Node;

	struct CountedNodePtr
	{
		int32 externalCount = 0;
		Node* ptr = nullptr;
	};

	struct Node
	{
		Node(const T& value) : data(make_shared<T>(value))
		{

		}

		shared_ptr<T> data;
		atomic<int32> internalCount = 0;
		CountedNodePtr next;
	};

public:
	void Push(const T& value)
	{
		CountedNodePtr node;
		node.ptr = new Node(value);
		node.externalCount = 1;
		// [!]
		node.ptr->next = _head;
		while (_head.compare_exchange_weak(node.ptr->next, node) == false)
		{

		}


	}

	shared_ptr<T> TryPop()
	{
		CountedNodePtr oldHead = _head;
		while (true)
		{
			// 참조권 획득 (externalCount를 현 시점 기준 +1 한 애가 이김)
			IncreaseHeadCount(oldHead);
			// 최소한 externalCount >= 2 일테니 삭제 x (안전하게 접근할 수 있음)
			Node* ptr = oldHead.ptr;
			
			// 데이터 없음
			if (ptr == nullptr)
				return shared_ptr<T>();

			// 소유권 획득 (ptr->next로 head를 바꿔치기 한 애가 이김)
			if (_head.compare_exchange_strong(oldHead, ptr->next))
			{
				shared_ptr<T> res;
				res.swap(ptr->data);

				// external : 1 -> 2(+1) -> 4(나+1, 남+2)
				// internal : 0

				// 나 말고 또 누가 있는가?
				const int32 countIncrease = oldHead.externalCount - 2;

				// fetch_add는 실행 후, 이전 값 리턴함
				if (ptr->internalCount.fetch_add(countIncrease) == -countIncrease)
					delete ptr;

				return res;
			}
			else if (ptr->internalCount.fetch_sub(1) == 1)
			{
				// 참조권은 얻었으나, 소유권은 실패 -> 뒷수습은 내가 한다.
				delete ptr;
			}
		}
	}

private:
	void IncreaseHeadCount(CountedNodePtr& oldCounter)
	{
		while (true)
		{
			CountedNodePtr newCounter = oldCounter;
			newCounter.externalCount++;
			if (_head.compare_exchange_strong(oldCounter, newCounter))
			{
				oldCounter.externalCount = newCounter.externalCount;
				break;
			}
		}
	}

private:
	atomic<CountedNodePtr> _head;

};

 

핵심은 externalCount로 참조권을, internalCount로 소유권을 파악하는 것이고, 경합 중인 쓰레드 개수를 파악하는 부분이다.

 

lock_free programming은 어렵다. 검증이 안된 코드를 사용하는 것은 프로그래밍 디버깅을 매우 어렵게 만드는 원인이 된다. 믿을 수 있는 코드만 사용하자.

 

참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

Read-Writer lock  (0) 2021.09.03
ThreadManager  (0) 2021.09.01
Lock free stack #2  (0) 2021.08.26
Lock free stack  (0) 2021.08.25
Lock-based stack, queue  (0) 2021.08.11

관련글 더보기

댓글 영역