bdfgdfg

[온라인 서버] 네트워크 라이브러리 - Thread 클래스 본문

게임프로그래밍/서버 책

[온라인 서버] 네트워크 라이브러리 - Thread 클래스

marmelo12 2022. 1. 8. 16:47
반응형

Thread Class

 - Thread 클래스는 사용자가 쉽게 틱(Tick) 쓰레드를 관리하기 위해 만든다.

 - 틱 쓰레드는 정해진 시간마다 특정 연산을 하기 원할 때 쓰이는데 온라인 게임 서버에서는 꼭 필요한 기능.

  -> 일반적으로 좀비상태의 클라이언트의 연결처리를 할때 많이 쓰인다고 한다.

  -> 좀비상태는 사용자가 연결만 있고 아무것도 하지 않는 상태를 의미. 

  -> 이 상태는 클라이언트와 서버간의 연결 상태가 좋지않거나, 특정 사용자가 악의적인 목적으로 이런 상태를 만들 수        있다.

  -> 그래서 보통 클라이언트에서는 일정 시간마다 패킷을 보내고 서버에서는 일정 시간안에 그 패킷이 도착하지 않으면

      클라이언트와의 연결을 끊어버린다. (좀비상태 방지 가능)

 - 틱 쓰레드는 위의 역할뿐만 아니라 온라인 게임 서버에서 동기화가 필요한 작업을 할 떄 서버의 시간을 기준으로 하는

   데 이때도 서버 틱(Server Tick)이란 단위를 두어 처리할 수 있도록 해준다.

 

간단하니 바로 헤더와 cpp파일을 본다.

#pragma once

#include "std.h"

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

public:
	// 틱 쓰레드는 waitTick / 1000 만큼 대기하면서 실행.
	// 즉 waitTick / 1000 시간마다 자식의 OnProcess가 처리 된다.
	bool CreateThread(DWORD waitTick);
	void DestroyThread();
	void Run();
	void Stop();
	void TickThread();
	virtual void OnProcess() abstract; // 순수 가상함수.
	// 몇번의 틱이 돌았는지 반환.
	inline DWORD GetTickCount() { return m_tickCount; }
	bool IsRun() { return m_bRunState; }

protected:
	HANDLE		m_hThread; // 쓰레드 핸들
	WSAEVENT	m_hQuitEvent; // 이벤트 핸들 (쓰레드 종료를 위해 존재한다)
	bool		m_bRunState;
	DWORD		m_waitTick;
	DWORD		m_tickCount;

};
#include "Thread.h"

Thread::Thread() : m_hThread(INVALID_HANDLE_VALUE),m_bRunState(false),m_tickCount(0),m_waitTick(0)
{
	// manual-reset, non-signaled상태로 생성.
	m_hQuitEvent = ::WSACreateEvent();
}

Thread::~Thread()
{
	::CloseHandle(m_hQuitEvent);
	if (m_hThread)
		::CloseHandle(m_hThread);
}

unsigned int WINAPI CallTickThread(LPVOID p) // this가 넘어온다.
{
	Thread* pTickThread = (Thread*)p;
	pTickThread->TickThread(); // 쓰레드는 틱 쓰레드 함수를 호출한다. 

	return 0;
}


bool Thread::CreateThread(DWORD waitTick) // 대기할 틱이 인자로 넘어온다.
{
	UINT uiThreadId = 0;
	// 대기상태로 쓰레드 생성
	m_hThread = (HANDLE)::_beginthreadex(nullptr, 0, CallTickThread, this, CREATE_SUSPENDED, &uiThreadId);
	if (m_hThread)
	{
		// 로그 클래스 추가 예정 (에러처리)
		return false;
	}

	m_waitTick = waitTick;
	return true;
}

void Thread::DestroyThread()
{
	Run(); // 쓰레드가 멈춘상태일 수 있으므로. 먼저 Run을 호출(돌고있다해도 조건체크를 통해 무시된다)
	::SetEvent(m_hQuitEvent); // 이벤트를 signaled상태로 바꾼다. -> 이 이벤트가 켜지면 쓰레드는 TickThread함수에서 빠져나온다.
	::WaitForSingleObject(m_hThread, INFINITE); // 쓰레드가 종료될때까지 대기한다.
}

void Thread::Run()
{
	// 쓰레드를 동작시킨다. 
	if (m_bRunState == false)
	{
		m_bRunState = true;
		::ResumeThread(m_hThread);
	}
}

void Thread::Stop()
{
	// 쓰레드를 멈춘다.
	if (m_bRunState == true)
	{
		m_bRunState = false;
		::SuspendThread(m_hThread);
	}
}

void Thread::TickThread()
{
	// 쓰레드가 처리하는 메인 함수.
	// 자식 클래스의 OnProcess를 호출한다.
	while (true)
	{
		DWORD ret = ::WaitForSingleObject(m_hQuitEvent, m_waitTick); // waitTick만큼 대기.
		if (ret == WAIT_OBJECT_0) // 만약 m_hQuitEvent가 signaled라면 (DestroyThread함수가 호출되었다는 소리)
		{
			break; // 빠져나온다.
		}
		else if(ret == WAIT_TIMEOUT) // 그게아니라 타임아웃이라면. -> 틱만큼 대기하다 OnProcess호출. (틱만큼 대기 -> OnProcess호출이 반복)
		{
			++m_tickCount;
			OnProcess();
		}
	}
}

 

먼저 생성자, 소멸자,쓰레드의 메인함수를 본다.

Thread::Thread() : m_hThread(INVALID_HANDLE_VALUE),m_bRunState(false),m_tickCount(0),m_waitTick(0)
{
	// manual-reset, non-signaled상태로 생성.
	m_hQuitEvent = ::WSACreateEvent();
}

Thread::~Thread()
{
	::CloseHandle(m_hQuitEvent);
	if (m_hThread)
		::CloseHandle(m_hThread);
}

unsigned int WINAPI CallTickThread(LPVOID p) // this가 넘어온다.
{
	Thread* pTickThread = (Thread*)p;
	pTickThread->TickThread(); // 쓰레드는 틱 쓰레드 함수를 호출한다. 

	return 0;
}

생성자에서 이벤트를 생성하고 있는데.

위의 이벤트는 DestroyThread함수에서 생성한 이벤트를 signaled상태로 바꾸어 쓰레드가 TickThread함수를 빠져나오게 한다. (쓰레드 종료)

 

그리고 쓰레드 메인 함수에서는 인자로 넘어온(this) 객체를 통해 TickThread함수를 호출하게 된다.

bool Thread::CreateThread(DWORD waitTick) // 대기할 틱이 인자로 넘어온다.
{
	UINT uiThreadId = 0;
	// 대기상태로 쓰레드 생성
	m_hThread = (HANDLE)::_beginthreadex(nullptr, 0, CallTickThread, this, CREATE_SUSPENDED, &uiThreadId);
	if (m_hThread)
	{
		// 로그 클래스 추가 예정 (에러처리)
		return false;
	}

	m_waitTick = waitTick;
	return true;
}

void Thread::DestroyThread()
{
	Run(); // 쓰레드가 멈춘상태일 수 있으므로. 먼저 Run을 호출(돌고있다해도 조건체크를 통해 무시된다)
	::SetEvent(m_hQuitEvent); // 이벤트를 signaled상태로 바꾼다. -> 이 이벤트가 켜지면 쓰레드는 TickThread함수에서 빠져나온다.
	::WaitForSingleObject(m_hThread, INFINITE); // 쓰레드가 종료될때까지 대기한다.
}

쓰레드의 생성. 위에서 말한 CallTickThread를 호출하며 인자로 this를 넘겨주고 있다.

 -> this를 넘기는 이유는 쓰레드 메인함수는 전역함수이므로.

그리고 CREATE_SUSPENDED 인자를 넘겨 쓰레드가 바로 실행되지 않고 대기상태로 만들어준다.

 -> 사용자가 원하는 시점에서 Run함수를 호출시켜 쓰레드가 돌아가게끔 한다.

마지막으로 CreateThread함수를 호출하면서 넘긴 인자값인 Tick을 멤버에 저장한다.

 -> 기본단위는 waitTick / 1000이며 (밀리세컨드 1 / 1000) 1000을 넘겨주면 1초마다 대기하며 틱 쓰레드 함수를 돈다.

 

DestroyThread함수는 TickThread 함수를 돌던 쓰레드를 이벤트를 통해 빠져나오게하여 쓰레드를 종료시킨다.

void Thread::Run()
{
	// 쓰레드를 동작시킨다. 
	if (m_bRunState == false)
	{
		m_bRunState = true;
		::ResumeThread(m_hThread);
	}
}

void Thread::Stop()
{
	// 쓰레드를 멈춘다.
	if (m_bRunState == true)
	{
		m_bRunState = false;
		::SuspendThread(m_hThread);
	}
}

각각 쓰레드를 멈추게 하는 기능 동작하게 하는 기능.

 

void Thread::TickThread()
{
	// 쓰레드가 처리하는 메인 함수.
	// 자식 클래스의 OnProcess를 호출한다.
	while (true)
	{
		DWORD ret = ::WaitForSingleObject(m_hQuitEvent, m_waitTick); // waitTick만큼 대기.
		if (ret == WAIT_OBJECT_0) // 만약 m_hQuitEvent가 signaled라면 (DestroyThread함수가 호출되었다는 소리)
		{
			break; // 빠져나온다.
		}
		else if(ret == WAIT_TIMEOUT) // 그게아니라 타임아웃이라면. -> 틱만큼 대기하다 OnProcess호출. (틱만큼 대기 -> OnProcess호출이 반복)
		{
			++m_tickCount;
			OnProcess();
		}
	}
}

쓰레드가 Run상태가 되고 쓰레드 메인함수인 CallTickThread에 진입하면 위의 함수가 호출이 된다.

처음에 CreateThread함수를 호출할 때 인자로 넘긴 waitTick값만큼 쓰레드는 대기하면서 OnProcess함수를 호출하게 된다.

 -> 즉 1000을 넘겼다면 1초마다 OnProcess함수를 처리.

OnProcess함수는 순수 가상함수로 자식에서 재정의 해야한다.

 

그리고 DestroyThread함수 호출을 통해 m_hQuitEvent멤버가 signaled상태가 되면 WAIT_OBJECT_0을 반환하고 함수를 빠져나온다 -> 종료

 

 

TickThread를 사용한 간단한 예제.

(헤더 + cpp파일 이어서)

//------HEADER---------
class TickThreadTest : public Thread
{
public:
    void OnProcess() override;
};
//------HEADER---------


//------CPP---------
void TickThreadTest::OnProcess()
{
    std::cout << "현재 틱 횟수 : " << m_tickCount << std::endl;
}
//------CPP---------

이 처럼 TickThread함수는 자식의 OnProcess함수를 호출하는데 단순히 얼마나 틱 시간안에 몇번이나 이 함수가 호출될지 정의한 함수.

 

int main()
{
    TickThreadTest t;
    t.CreateThread(1000); // waitTick값을 넘겨준다. 1000 / 1000이므로 1초 마다 OnProcess호출.
    t.Run(); // 대기중이던 쓰레드를 깨운다.
    ::Sleep(10000); // 10초대기
    t.DestroyThread();

    return 0;
}

 

내 컴퓨터는 10초를 대기하면서 총 밑의 사진과 같이 호출 되었음.

본문은 강정중님의 온라인 게임 서버 서적을 참고하였습니다.

반응형
Comments