지난 시간까지 Job을 탬플릿을 이용해서 보다 편리하게 처리하는 방법을 알아봤다.

이 방법을 최종적으로 사용할 것은 아니지만 c++이해도가 높아질 수 있는 공부가 됐다.

 

c++11에서 들어온 매우 강력한 기능이 있는데, 람다식과 functional 이다.

void HelloWorld(uint32 a, uint32 b)
{
	cout << a << b << "hello world" << endl;
}

int main()
{
	PlayerRef player = MakeShared<Player>();
	std::function<void()> func = [=]()
		{
			HelloWorld(1, 2);
			GRoom.Enter(player);
		};
	//한참 뒤
	func();
}

위와 같이 function객체에 람다식을 넣어놓으면 똑같이 작동시킬 수 있다.

 

이렇게 쉬운 방법이 있는데 왜 어려운 functor를 배웠냐?

functor를 배우면 람다함수의 사용을 이해하기 훨씬 좋다.

 

일반적인 함수는 함수포인터를 저장한다 해도 인자는 따로 저장해둬야 하기 때문에 functor에 함수 포인터와 tuple에 저장해뒀다가 나중에 꺼내 쓰는 식으로 사용했다. 그런데 놀랍게도 람다함수는 그냥 해결된다. helloworld 함수에 넣은 1, 2도 따로 저장할 필요 없이 익명 함수 내에 전달돼서 저장하는거나 마찬가지로 실행되는 것.

 

이런 것을 프로그래밍 언어에서 Closure 라고 하는데, 이런 코드를 만들면 컴파일러가 내부적으로 어떤 클래스를 만들어 데이터들을 저장해놓는 식으로 작동한다고 한다.

 

하지만 단점도 있는데, c++과 궁합이 안맞는 부분이 있다고 한다.

[ ] 안에 캡쳐 모드를 지정하는데, =는 복사해서 넣겠다, &는 참조값으로 넣겠다 라는 뜻. 하나씩 지정할 수도 있음.

변수이름만 쓰면 복사, &이름 쓰면 참조로.

 

c++에서 람다함수가 왜 안어울리냐? 만들자 마자 바로 호출하면 문제가 없다. 우리가 job을 사용하는 방식을 생각해보면 job을 등록해 놨다가 나중에 실행하는 방식인데 생명주기 관리가 까다롭기 때문. 그냥 참조값으로 넣게된다면 쉐어드포인터라 할지라도 문제가 생긴다.

 

그리고 멤버함수 안에서 사용할 때도 문제가 발생하기 딱 좋은 요소가 있다.

class Knight
{
public:
	void HealMe(uint32 value) {
		_hp += value;
		cout << "Hp : " << value << endl;
	}
	void Test()
	{
		auto job = [=]()
			{
				HealMe(_hp);
			};
	}
private:
	int32 _hp = 100;
};

위와 같은 코드가 있을 때 람다식 속 _hp는 정해진 정수니까 괜찮은 것 아닌가? 할 수 있지만 실제로는 아래와 같은 코드로 작성된 것으로 this 즉 Knight의 객체가 소멸됐다면 this라는건 존재하지 않으니 문제가 생긴다.

auto job = [this]()
    {
        HealMe(this->_hp);
    };

 

그러니 어지간 해서는 [=] 와 같은 코드는 사용하지 말고, [this] 와 같이 늘 특정지어 쓰도록 하자.

근데 그래도 아직 댕글링 포인터 문제는 해결되지 않았다. 예전에 사용했던 enable_shared_from_this를 상속해서 람다함수에 복사해서 써야한다.

class Knight : public enable_shared_from_this<Knight>
{
public:
	void HealMe(uint32 value) {
		_hp += value;
		cout << "Hp : " << value << endl;
	}
	void Test()
	{
		auto job = [self = shared_from_this()]()
			{
				self->HealMe(self->_hp);
			};
	}
private:
	int32 _hp = 100;
};

이렇게 하면 job이 유지되는 동안에는 self에서 래퍼런스 카운트를 잡고있기 때문에 댕글링 포인터 문제를 피할 수 있다.

 

람다에서 쉐어드포인터를 섞어 쓰면 메모리 릭이 발생하기 때문에 절대 사용하면 안된다는 낭설이 있는데, 사실이 아니라고 한다.

다만 스마트포인터 사용에 있어서 실수할 여지가 있는것은 사실이기 때문에 이것을 래핑해서 사용하는 방법을 배워보자.

 

 


 

using CallbackType = std::function<void()>;

class Job
{
public:
	Job(CallbackType&& callback) : _callback(std::move(callback))
	{
	}

	template<typename T, typename Ret, typename... Args>
	Job(shared_ptr<T> owner, Ret(T::*memFunc)(Args...), Args&&... args)
	{
		_callback = [owner, memFunc, args...]()
			{
				(owner.get()->*memFunc)(args...);
			};
	}

	void Execute()
	{
		_callback();
	}
private:
	CallbackType _callback;
};

Job 클래스는 함수를 콜백으로 들고있다가 나중에 실행시켜주는 역할이다.

다만 앞서말한 댕글링 포인터 문제를 차단하기 위해 멤버함수에 대한 처리를 오버로딩으로 따로 하고있다.

  • function<void(void)> 를 사용할 수 있는 이유는 람다함수 안에서 뭘 쓰던 람다함수 자체는 void(void) 이기 때문.
  • 첫번째 생성자는 job을 생성할 때 람다함수나 function의 임시 객체를 직접 넣는 경우.
    람다함수를 넣으면 function<void()> 임시객체로 (암시적)변환되고 그리고 CallbackType&&에 바인딩된다.
  • 두번째는 객체의 쉐어드포인터, 멤버함수, arg를 받아, 멤버함수를 job queue에 넣어 처리할 때 댕글링 포인터 문제가 발생하지 않도록 방지한 생성자.
class JobQueue
{
public:
	void Push(JobRef job)
	{
		WRITE_LOCK;
		_jobs.push(job);
	}

	JobRef Pop()
	{
		WRITE_LOCK;
		if (_jobs.empty())
			return nullptr;
		JobRef ret = _jobs.front();
		_jobs.pop();
		return ret;
	}

private:
	USE_LOCK;
	queue<JobRef> _jobs;
};

JobQueue는 이전에 만든 그대로.

JobRef를 저장하고, 꺼내는 역할

class JobSerializer : public enable_shared_from_this<JobSerializer>
{
public:
	void PushJob(CallbackType&& callback)
	{
		auto job = ObjectPool<Job>::MakeShared(std::move(callback));
		_jobQueue.Push(job);
	}

	template<typename T, typename Ret, typename... Args>
	void PushJob(Ret(T::* memFunc)(Args...), Args... args)
	{
		shared_ptr<T> owner = static_pointer_cast<T>(shared_from_this());
		auto job = ObjectPool<Job>::MakeShared(owner, memFunc, std::forward<Args>(args)...);
		_jobQueue.Push(job);
	}

	virtual void FlushJob() abstract;
protected:
	JobQueue _jobQueue;
};

JobSerializer는 Job을 편하게 사용하기 위한 클래스

Room 클래스에서 Push와 Flush를 만들어 쓰고있는데, 모든 클래스에 이것들을 매번 만들기란 귀찮은 일이다.

JobSerializer를 상속받아서 Push와 Flush를 손쉽게 쓸 수 있게 만들어준다.

  • 1번 PushJob은 function 임시객체나 람다함수를 바로 넣는 용도
  • 2번은 객체의 멤버함수를 job으로 등록하는 경우를 처리하는 용도

내부에서 job을 shared_ptr로 만들어서 JobQueue에 등록해준다.

class Room : public JobSerializer
{ /*...*/

Room은 JobSerializer를 상속받았기 때문에 사용은 그냥 Room객체->PushJob, FlushJob 이렇게만 사용하면 된다. (FlushJob은 구현해야함. JobQueue에서 pop해서 execute하면 된다.)

 

- 사용 부분

bool Handle_C_ENTER_GAME(PacketSessionRef& session, Protocol::C_ENTER_GAME& pkt)
{
	/*...*/
	PlayerRef player = gameSession->_players[index];// read only?
	GRoom->PushJob(&Room::Enter, player);
	/*...*/
}

int main()
{
	/*...*/
	while (true)
	{
		GRoom->FlushJob();
		this_thread::sleep_for(1ms);
	}
	/*...*/
}

 

이렇게 Job을 처리하는 방법을 또 한가지 배워봤다.

'강의 수강 > 게임서버(1)' 카테고리의 다른 글

73. JobQueue #4  (0) 2025.08.23
강의를 듣고 다시 확인할 것들 모음  (0) 2025.08.23
71. JobQueue #2  (0) 2025.08.19
70. JobQueue #1  (0) 2025.08.19
69. 채팅 실습  (0) 2025.08.18

+ Recent posts