bdfgdfg

쓰레드와 동기화기법 및 쓰레드 매니저 본문

게임프로그래밍/서버 책

쓰레드와 동기화기법 및 쓰레드 매니저

marmelo12 2021. 12. 16. 17:40
반응형

쓰레드

- 운영체제 관점에서의 실행단위는 프로세스. 쓰레드는 그 프로세스 내부의 실행 흐름

- 쓰레드는 O/S에서의 스케줄링 단위 (CPU의 작업단위)

- 메인 쓰레드(main함수에 진입하는 쓰레드 OS를 통해 하나 할당)를 포함한 둘 이상의 쓰레드를 멀티쓰레드라 함.

- 컨텍스트 스위칭이 일어나는 대상은 쓰레드(Thread)

 

생성방법

CreateThead or _beginThreadex함수 호출.

인자값으로 보안,스택크기,쓰레드진입함수,쓰레드에 넘겨줄 인자값,쓰레드 상태,쓰레드 ID값

-> _beginThreadex함수가 C표준함수 (HANDLE로 변환필요)

 

객체 동기화

둘 이상의 쓰레드가 공유자원(쓰레드는 스택 메모리를 제외한 프로세스의 나머지 메모리영역을 공유한다)

에 동시접근하여 값의 수정등을 할 때 의도치않은 문제가 발생할 수 있음 -> 컨텍스트 스위칭등

-> 둘이상의 쓰레드가 공유자원에 동시에 접근하여 문제가 발생할 수 있는 코드블럭을 임계영역(CriticalSection)

 

그렇기에 임계영역에 진입할땐 하나의 쓰레드만 접근하게하고, 다른 쓰레드의 진입을 막아 대기시키는것을

동기화(Synchronize)라고 함.

 

동기화 기법은 크게 4가지

1. CriticalSection 함수사용 (유저모드 동기화)
2. Mutex 3.Semaphore 4.Event (커널모드 동기화)

 

CriticalSection과 Mutex는 매우 흡사.

Semaphore는 동시에 진입가능한 쓰레드의 수를 늘릴 수 있음.

-> 단 Mutex와 Semaphore는 커널모드 동기화 기법

-> Signaled,Non-signaled 상태를 통해 동기화를 적용.

--> Signaled상태일 때 쓰레드가 진입후 해당 커널 오브젝트(Mutex,Semapohre)는 Non-signaled상태로 바뀌는

      auto-reset모드

 

Event 커널오브젝트의 경우 auto-reset모드,Manual-reset(Signaled상태가 되어 빠져나와도 그대로 Signaled상태를 유지)모드 둘다 고를 수 있음.

 

CrticalSection,Mutex,Semaphore등에서 프로그래머의 실수로 발생할 수 있는 문제가 있는데

임계영역으로 진입 후 해당 락(Lock)들을 풀어주지않으면 대기중이던 쓰레드는 무한정대기하는 교착상태(DeadLock)에

빠짐.

 

정상적인 동기화 순서 예

1. 화장실에 들어가기 위해 열쇠 획득

2. 화장실에 들어가 문을 잠굼 (화장실에 들어가고 싶어하는 다른사람들은 문앞에서 대기)

3. 볼일을 다보면 문을열고 나와 열쇠를 반납. (위 반복)

 

비정상적인 동기화 순서 예

1. 화장실에 들어가기 위해 열쇠 획득

2. 화장실에 들어가 문을 잠굼 (화장실에 들어가고 싶어하는 다른사람들은 문앞에서 대기)

3. 볼일을 다보면 문을 통해 나가지않고 옆 창문을 부수고 나감. (문앞 사람들 무한대기)

-> Dead Lock발생

 

// CriticalSection 사용예만 봄

CRITICAL_SECTION cs;
int sum = 0;

void TestThread()
{
	::InitializeCriticalSection(&cs); // CriticalSecion객체도 해제해줘야함. DeleteCrticalSection함수.
	Test();
}

void Test()
{

	::EnterCriticalSection(&cs);
	for (int i = 0; i < 100'0000; ++i)
	{
		sum++;
	}
	::LeaveCriticalSection(&cs);
}

마지막에 LeaveCriticalSection함수가 빠졌다면 다른 쓰레드는 교착상태에 빠질 수 있다.

 

+ 병목현상

: Critical Section의 동기화 기법을 통해서 하나의 쓰레드가 진입하고 나머지 쓰레드가 대기중일 때,

임계영역의 처리가 길어지면 앞에서 기다리던 여러 쓰레드들이 계속해서 대기하게 된다. -> 성능저하의 원인

(SpinLock)

동기화 클래스 ★

위와 같은 실수를 줄이기 위한 꼼수(?)로 한번 동기화 객체를 클래스로 래핑하여 사용한다

-> 즉 생성자와 소멸자를 사용

class Synchronized
{
public:
	Synchronized(CRITICAL_SECTION& cs);
	virtual ~Synchronized();
private:
	CRITICAL_SECTION* m_criticalSection;
};

좀 더 내부 내용이 있어야하지만 위만봐도 이해가 어렵지 않음.

-> c++11에서는 위와같은 방식인 Lock_guard가 존재한다.

 

쓰레드 매니저

class ThreadManager
{
public:
	static ThreadManager* GetInstance() { if (m_threadinstance == nullptr) m_threadinstance = new ThreadManager(); return m_threadinstance; }
	static void ReleaseInstance();
	void   Join(); // 생성된 쓰레드들이 모두 종료될때까지 대기.
	// Spawn - 실제로 쓰레드를 할당하기 위해 호출하는 함수. 
	HANDLE Spawn(LPTHREAD_START_ROUTINE threadMainFunc, LPVOID parameter, DWORD* threadID);
private:
	ThreadManager();
	virtual ~ThreadManager();

	static ThreadManager* m_threadinstance;
	std::list<HANDLE>	  m_listThreadHandle; // Spawn에서 생성한 Thread의 핸들값을 연결리스트에 저장.
	using ListPosition = std::list<HANDLE>::iterator;
};

ThreadManager* ThreadManager::m_threadinstance = nullptr;

 

쓰레드매니저라는 싱글턴패턴 (프로세스내의 단일객체)

-> 실제로 여기서 Thread를 생성하며 내부적으로 list를 통해 쓰레드핸들을 담아 관리한다.

-> Join등 쓰레드 대기함수도 존재

#include "ThreadManager.h"
ThreadManager* ThreadManager::m_threadinstance = nullptr;
void ThreadManager::ReleaseInstance()
{
	if (m_threadinstance != nullptr)
	{
		delete m_threadinstance;
		m_threadinstance = nullptr;
	}
}

void ThreadManager::Join()
{
	ListPosition pos = m_listThreadHandle.begin(), posPrev;

	while (pos != m_listThreadHandle.end())
	{
		posPrev = pos++; // posPrev는 현재 쓰레드.
		::WaitForSingleObject(*posPrev, INFINITE); // 선택된 쓰레드가 종료되길 기다린다.
		m_listThreadHandle.erase(posPrev);
	}
}

HANDLE ThreadManager::Spawn(LPTHREAD_START_ROUTINE threadMainFunc, LPVOID parameter,DWORD* threadID)
{
	HANDLE hThread;

	hThread = ::CreateThread(0, 0, threadMainFunc, parameter, 0, threadID);
	m_listThreadHandle.push_back(hThread);
	return hThread;
}

ThreadManager::~ThreadManager()
{
	ListPosition pos;
	while (m_listThreadHandle.size() > 0)
		m_listThreadHandle.erase(m_listThreadHandle.begin()); 
	
}

 

쓰레드 클래스.

-> 이 쓰레드 클래스를 상속받아 임의의 쓰레드 클래스에서 가상함수 Run만 재정의하여 사용.

#include "std.h"

class Thread
{
public:
	Thread();
	virtual ~Thread();
public:
	virtual void Run() = 0; // 자식에서 재정의해서 사용
	void	Begin();
	DWORD   GetThreadID();
protected:
	HANDLE	m_handleThread;
private:
	static	DWORD	HandleRunner(LPVOID parameter); // 쓰레드의 메인함수.
	bool	m_isStarted; // 쓰레드 시작여부
	DWORD	m_threadID; // 쓰레드 ID
};
#include "Thread.h"
#include "ThreadManager.h"

Thread::Thread() : m_isStarted(false),m_threadID(0),m_handleThread()
{

}

Thread::~Thread()
{
}

void Thread::Begin()
{
	if (m_isStarted) // 이미 실행중인 쓰레드 객체라면 무시.
		return;
	
    // 쓰레드매니저를 통해서 쓰레드 생성 쓰레드는 HandleRunner함수에 진입
    // 쓰레드 메인함수에서 각 쓰레드 객체의 재정의된 Run을 호출하기위해 자기자신(this)도 파라미터로넘긴다.
	m_handleThread = ThreadManager::GetInstance()->Spawn(HandleRunner, this, &m_threadID);
	m_isStarted = true;
}

DWORD Thread::GetThreadID()
{
	return m_threadID;
}

DWORD Thread::HandleRunner(LPVOID parameter)
{
	// 넘겨받은 Parameter(쓰레드클래스 객체(정확히는 상속받은 자식 쓰레드 객체))
	Thread* thread = static_cast<Thread*>(parameter);

	thread->Run(); // virtual. 자식의 Run호출이 된다.
	return 0;
}

 

 

이제 테스트를 위해 Thread클래스를 상속받은 자식 클래스들 생성.

#include "Thread.h" // 상속

class Synchronized // 동기화 클래스
{
public:
	Synchronized(CRITICAL_SECTION& cs) : m_criticalSection(&cs)
	{
		::EnterCriticalSection(m_criticalSection);
	}
	virtual	~Synchronized() 
	{
		::LeaveCriticalSection(m_criticalSection);
	}
private:
	CRITICAL_SECTION* m_criticalSection;
};

class TestThreads : public Thread
{
public:
	TestThreads() {} ;
	virtual ~TestThreads() {} ;
public:
	virtual void Run();
};

 

메인함수가 존재하는 cpp파일에서 Run함수를 재정의하여 사용해본다.

#pragma once
#include "Thread.h" // 상속

class Synchronized
{
public:
	Synchronized(CRITICAL_SECTION& cs) : m_criticalSection(&cs)
	{
		::EnterCriticalSection(m_criticalSection);
	}
	virtual	~Synchronized() 
	{
		::LeaveCriticalSection(m_criticalSection);
	}
private:
	CRITICAL_SECTION* m_criticalSection;
};



class TestThreads : public Thread
{
public:
	TestThreads() {} 
	virtual ~TestThreads() {} 
public:
	virtual void Run();
};

class TestThreads2 : public Thread
{
public:
	TestThreads2() {}
	virtual ~TestThreads2() {}
public:
	virtual void Run();
};

 

main cpp

#include "TestThreads.h"
#include "ThreadManager.h"

CRITICAL_SECTION cs;
int sum = 0;
void TestThreads::Run() // sum++
{
	Synchronized sync(cs);
	for (int i = 0; i < 1'0000000; ++i)
	{
		sum++;
	}
}

void TestThreads2::Run() // sum--
{
	Synchronized sync(cs);
	for (int i = 0; i < 1'0000000; ++i)
	{
		sum--;
	}
}


int main()
{
	::InitializeCriticalSection(&cs);

	TestThreads t1;
	TestThreads2 t2;
	t1.Begin();
	t2.Begin();
	ThreadManager::GetInstance()->Join(); // 쓰레드 메인함수들이 끝날때까지 대기.

	std::cout << "t1 쓰레드 아이디 : " << t1.GetThreadID() << std::endl;
	std::cout << "t1 쓰레드 아이디 : " << t2.GetThreadID() << std::endl;
	std::cout << "sum : " << sum;
	return 0;
}

결과.

만약 동기화를 하지 않을시

 

반응형
Comments