언어/Basic C++

[Basic C++] #61_객체 풀, Obejct Pooling

Hardii2 2022. 10. 7. 03:25

 

[Basic C++] #61_객체 풀, Obejct Pooling

 

C++의 "디자인 효율성"에 대해 알아보겠습니다.

"전문가를 위한 C"의 22 항목, "효과적인 메모리 관리"에 해당하는 내용입니다.

 


 

객체 풀링

1. 개념

  • 객체가 필요할 때마다 직접 생성하는 것이 아니라, 시작 시점에 한 번만 생성됩니다.
  • 필요할 때마다 객체 풀에 요청하며, 수명이 끝난 객체는 객체 풀에 되돌려 놓습니다.
  • 즉, 객체의 생성자를 매번 필요할 때마다 호출하지 않고, 시작 시점에 미리 준비해 놓습니다.

 

2. 언제?

  • 프로그램이 구동되는 동안 빈번하게 발생하는 "생성자"와 "소멸자" 호출을 피하기 위한 메커니즘.
  • 개수가 많고, 비교적 수명이 짧은 타입의 객체들의 사용은 "객체 풀" 사용을 고려해야 합니다!

 

예제

1. 목표

  1. 객체들을 담을 컨테이너는 STL 컨테이너들 중 "Queue"를 활용합니다.
  2. 객체 풀에 정해진 개수만큼(MAX_POOL_SIZE) 객체를 할당합니다.
  3. 객체가 요구될 때마다 넘겨줍니다.
  4. 요청이 들어왔을 때, 객체 풀에 Extra 객체가 남아있지 않다면, 새로운 객체를 객체 풀에 할당합니다. 
  5. 요청이 들어오면, 스마트 포인터를 통해 객체를 넘겨줍니다.

2. ObjectPool.h

#include <queue>
#include <iostream>
#include <memory>
#include <stdexcept>
#define MAX_POOL_SIZE 10
using namespace std; 

template<typename T>
class ObjectPool
{
public:
	// 1. 생성자
	ObjectPool();

	// 2. Type Aliasing
	using Object = shared_ptr<T>;

	// 3. 객체 할당 메서드
	Object AcquireObject();

private:
	// 4. 할당 가능한 객체 풀
	queue<unique_ptr<T>> FreePool;
	// 5. 할당 가능한 객체 개수
	size_t currentPoolSize;
	// 6. 할당 가능한 최대 객체 개수
	static const size_t maxPoolSize = MAX_POOL_SIZE;

private:
	//  7. 새로운 객체를 풀에 할당하는 메서드
	void AllocateObject();

};
  • 객체 풀 클래스를 템플릿 클래스로 정의합니다.
  • 반환하는 객체를 "shared_ptr"로 감싸줍니다!
  • 풀에 새로 할당하는 객체는 "unique_ptr"로 감싸줍니다!

 

3. ObjectPool.cpp

#include <queue>
#include <iostream>
#include <memory>
#include <stdexcept>
#define MAX_POOL_SIZE 10
using namespace std; 

template<typename T>
class ObjectPool
{
public:
	// 1. 생성자
	ObjectPool(size_t initSize);

	// 2. Type Aliasing
	using Object = shared_ptr<T>;

	// 3. 객체 할당 메서드
	Object AcquireObject();

private:
	// 4. 할당 가능한 객체 풀
	queue<unique_ptr<T>> FreePool;
	// 5. 할당 가능한 객체 개수
	size_t currentPoolSize;
	// 6. 할당 가능한 최대 객체 개수
	static const size_t maxPoolSize = MAX_POOL_SIZE;

private:
	//  7. 새로운 객체를 풀에 할당하는 메서드
	void AllocateObject(int newObjectNum);

};

 

4. ObjectPool<T>::ObjectPool, 생성자 정의부

template<typename T>
ObjectPool<T>::ObjectPool(size_t initSize)
{
	if (initSize == 0)
		throw invalid_argument("Pool Size Can't be 0");

	currentPoolSize = initSize;

	// 최초 할당할 객체의 개수를 AllocateObject 메서드에 넘겨줍니다.
	AllocateObject(crrentPoolSize);
}

 

5. ObjectPool<T>::Object ObjectPool<T>::AcquireObject, 객체 반환 메서드

template<typename T>
ObjectPool<T>::Object ObjectPool<T>::AcquireObject()
{
	// Pool이 비어있다면, 새로운 객체를 할당합니다.
	if (FreePool.empty() == true)
		AllocateObject(1);

	// 풀의 가장 앞에 있는 객체를 로컬 unique_ptr로 옮겨줍니다.
	unique_ptr<T> objPtr(move(FreePool.front()));
	FreePool.pop();

	// 로컬 unique_tpr를 shared_ptr로 옮깁니다.* 주의 : 람다 표현식 활용 + 커스텀 제거자
	// 커스텀 제거자 : 객체를 메모리에서 해제하지 않습니다!
	// 람다 표현식 : 객체를 풀에 재 삽입합니다.
	Object newObject(objPtr.release(), [this](T* t) {
		FreePool.push(unique_ptr<T>(t));
	});

	return newObject;
}
  • 간단하게 설명하자면, ObjectPool 에서 선언한 타입 에일리어싱, "using Object" 타입을 반환합니다.
  • unique_ptr에서 shared_ptr로 옮기는 과정에서, 객체를 메모리에서 해제하지 않는 것이 주요합니다.

 

6. void ObejctPool<T>::AllocateObject, 객체 할당 메서드

template<typename T>
void ObjectPool<T>::AllocateObject(int newObjectNum)
{
	for (int i = 0; i < newObjectNum; i++)
	{
		// emplace를 통해 즉석에서 객체를 생성해 삽입합니다.
		FreePool.emplace(make_unique<T>());
	}
}

 

객체 풀 활용

1. UserRequest.h

class UserRequest
{
public:
	UserRequest();
	virtual ~UserRequest();

	// 1. 유저 요청 할당 메서드
	// 2. 유저 요청 데이터 반환 메서드
	//...

private:
	//...
};

ObjectPool<UserRequest>::Object ObtainUserRequest(ObjectPool<UserRequest>& objPool)
{
	auto request = objPool.AcquireObject();

	return request;
}

 

2. Main

int main()
{
	ObjectPool<UserRequest> requestPool(10);

	for (size_t i = 0; i < 100; i++)
	{
		auto request = ObtainUserRequest(requestPool);
		// 처리
	}
	return 0;
}