반응형

 

 

전에 채팅 프로그램을 정리하는 글에서도 말했듯이 지금은 모든 함수에 락을 걸어놓는 원시적인 방법으로 구현해놓았다.

작은 규모의 프로젝트면 큰 문제가 없지만 큰 규모의 프로젝트라면 스레드간의 경합이 심해지게 되면서 큰 문제가 발생한다.

 

위와 같이 구현하게 될 경우, 만약 다수의 클라이언트가 우연히 같은 함수(ex : Broadcasting)를 처리하려고 시도하게되면, 

먼저 락을 잡고 처리중인  하나의 스레드가 일을 마칠동안 다른 스레드는 대기하면서 자원만 낭비할 것이고,

심지어 스핀락으로 구현했기 때문에 계속 루프를 돌며 cpu 자원만 고갈하게 될 것이다.

 

이를 해결하기 위해 우리는 커맨드 패턴을 사용한 Job 방식으로 수정할 것이다.

 

커맨드 패턴

  • 행위 패턴
  • 이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용할 수 있게하는 패턴
  • 실행될 기능을 캡슐화하으로써 기능의 실행을 요구하는 호출자 클래스와 실제 기능을 실행하는 수신자 클래스 사이의 의존성을 제거
  • 실행될 기능의 변경에도 호출자 클래스를 수정 없이 그대로 사용할 수 있도록 해줌

역할이 수행하는 작업

  • Command
    • 실행될 기능에 대한 인터페이스
    • 실행될 기능을 execute 메서드로 선언
  • ConcreteCommand
    • 실제로 실행되는 기능을 구현(Command 인터페이스를 실체화)
  • Invoker
    • 기능의 실행을 요청하는 호출자 클래스
  • Receiver
    • ConcreteCommand에서 execute 메서드를 구현할 때 필요한 클래스
    • ConcreteCommand에서 기능을 실행하기 위해 사용하는 수신자 클래스

 

어떤 요청을 객체로 담고 있다가 건네주는 것이 핵심.

 

이렇게 해주면 락을 잡으려고 모든 클라이언트가 같은 함수에서 경합하는 것이 아니라 순차적으로 일감(Job/Task)을 만들어서 일감만 넘긴 다음 빠져나와서 자기가 할 일을 하게 될 것이다.

 


 

 

위와 같은 내용을 커맨드 패턴으로 만들어서 기존 방식처럼 요청을 받자마자 바로 호출을 하는게 아닌

일감으로 한번 묶어 보도록 하자.

 

 

우선 어떤 느낌인지 이해하기 쉽게 가장 원시적인 방법으로 만들어 보자.

 

 

위와 같이 요청사항을 클래스로 만들어서 해당하는 행동들과 인자들을 저장한 뒤,

필요할 때 바로 호출할 수 있도록 하는것이 가장 핵심이고 기초이다.

 

 

그러면 이렇게 Job을 만들어 두고 나중에 필요할 때 호출 해주면 된다.

 


 

물론 위와 같은 방식에는 치명적인 단점이 있는데 바로 일감이 필요할 때 마다 클래스를 늘려줘야 한다는 것이다.

(그런데 실제로 이렇게 작업하는 프로젝트도 꽤 있다고 한다.)

 

일단 우리는 임시로 이 방법으로 Room을 개선해 볼 것이다.

 

 

Job 인터페이스

 

class IJob
{
public:
	virtual void Execute() {}
};

 

JobQueue

 

using JobRef = shared_ptr<IJob>;

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

 


 

수정된 Room.h

 

class Room
{
friend class EnterJob;
friend class LeaveJob;
friend class BroadcastJob;

private:
	void Enter(PlayerRef player);
	void Leave(PlayerRef player);
	void Broadcast(SendBufferRef sendBuffer);

public:
	void PushJob(JobRef job) { _jobs.Push(job); }
	void FlushJob();

private:
	JobQueue _jobs;
	map<uint64, PlayerRef> _players;
};

extern Room GRoom;

 

수정된 Room.cpp

 

#include "pch.h"
#include "Room.h"
#include "Player.h"
#include "gameSession.h"

Room GRoom;

void Room::Enter(PlayerRef player)
{
	_players[player->playerId] = player;
}

void Room::Leave(PlayerRef player)
{
	_players.erase(player->playerId);
}

void Room::Broadcast(SendBufferRef sendBuffer)
{
	for (auto& p : _players)
	{
		p.second->ownerSession->Send(sendBuffer);
	}
}

void Room::FlushJob()
{
	while (true)
	{
		JobRef job = _jobs.Pop();

		if (job == nullptr)
			break;

		job->Execute();
	}
}

 


 

Room에 필요한 Job 클래스

 

//Roob Jobs
class EnterJob : public IJob
{
public:
	EnterJob(Room& room, PlayerRef player) :_room(room), _player(player)
	{

	}
	
	virtual void Execute() override
	{
		_room.Enter(_player);
	}

public:
	Room& _room;
	PlayerRef _player;
};

class LeaveJob : public IJob
{
public:
	LeaveJob(Room& room, PlayerRef player) :_room(room), _player(player)
	{

	}

	virtual void Execute() override
	{
		_room.Leave(_player);
	}

public:
	Room& _room;
	PlayerRef _player;
};

class BroadcastJob : public IJob
{
public:
	BroadcastJob(Room& room, SendBufferRef sendBuffer) :_room(room), _sendBuffer(sendBuffer)
	{

	}

	virtual void Execute() override
	{
		_room.Broadcast(_sendBuffer);
	}

public:
	Room& _room;
	SendBufferRef _sendBuffer;
};

 

왜 이렇게 했는가?
원래 우리는 Room을 사용할 때 락을 잡고 사용했었다.
하지만 JobQueue방식을 사용하게 되면 더 이상 내부적으로 락을 사용하지 않을 것이다.
그래서 기본적인 함수(Enter,Leave,Broadcast)들은 마치 싱글쓰레드 환경인것 처럼 코딩해주게 될 것이다.
멀티쓰레드 환경에서는 무조건 일감(Job)으로 접근해야 한다.

분명 멀티 쓰레드 환경인데 싱글 쓰레드 환경인 것 처럼 코딩하면 
쓰레드간의 동시 접근이 생기지 않을까 라는 생각이 들 수 있다.
하지만 이런 Job방식을 사용하는 순간 기본적인 함수들은 직접적으로 접근하여 바로 호출하는 것이 아닌,
일감(Job)을 통해서만 접근하여 호출할 수 있다.
(정 외부 호출이 불안하면 private 으로 바꿔도 된다.)

 


 

 

그리고 ClientPacketHandler 코드를 위와 같이 수정해서 함수를 바로 호출하는게 아니라 일감만 요청하도록 수정하자.

 

 

 

그런 다음 메인 스레드에서 while문을 통해서 일감을 순차적으로 꺼내서 실행시켜 주면 멀티쓰레드 환경에서도 안전하고 정상적으로 동작하게 된다.

 

이렇게 스레드간의 경합을 최대한 줄이는 방법으로 수정하였다.

그렇다고 락을 아예 안잡는게 아니라 JobQueue에 넣고 뺄 때만 락을 잠깐 잡아주는 방식으로 경합을 줄였다.

반응형

'네트워크 프로그래밍' 카테고리의 다른 글

JobQueue(3)  (0) 2024.01.03
JobQueue (2)  (0) 2023.12.29
에코 채팅 프로그램  (0) 2023.10.02
ProtoBuf  (0) 2023.06.26
PacketSession  (0) 2023.04.15