bdfgdfg

IOCP(Input Ouput Completion Port) 본문

게임프로그래밍/서버 책

IOCP(Input Ouput Completion Port)

marmelo12 2021. 12. 17. 13:57
반응형

IOCP

- 입출력 완료 포트란 의미

- 입/출력 작업에 대한것을 처리해주는 오브젝트.

- IOCP도 커널오브젝트. 입출력 작업과 관련있는 핸들에서 발생하는 이벤트를 커널단에서 관리해줌.

 -> 단 모든 입출력 핸들이 사용가능한것은 아니고, 입출력 핸들이 비동기 작업이 가능해야한다.

 -> 그 이유는 비동기 입출력이 가능한 핸들의 이벤트를 IOCP가 대신 처리해주기 때문

 -> 즉 우리는 IOCP에게 작업을 맡겨두고 결과만을 우리가 나중에 확인하면 된다. (비동기방식)

 

IOCP의 동작

- IOCP는 메인 프로세스와 별도로 동작한다.

- 현재 입/출력 작업이 어떻게 진행되고 있는지 작업 프로세스에서는 알 필요가없다.

 -> 단지 작업의 결과만을 통보받는다 (우리가 나중에 확인한다)

 

참고 동기 입출력 vs 비동기 입출력

동기    입출력은 입출력 함수가 반환이 되어야 메인 프로세스가 이어서 진행가능.

비동기 입출력은 입출력 함수를 호출하자마자 반환.

그렇다면 여러개의 핸들로 작업을 할 때 어떻게 결과 통보만을 받아서 어느 핸들의 작업이 완료된것인지

판단할까?

-> 이 문제에 대한 해결책으로 IOCP에서는 완료 키(Completion Key)값을 사용

-> 보통 완료키 인자에 사용자 정의 구조체에 소켓핸들들을 담고★, (DWORD)형으로 형변환하여 등록 한다.

-> IOCP 내부에서 이 값들을 따로 저장함. (이 부분의 자료구조를 Device List라고한다)

-> 참고로 밑에서 보겠지만 결과를 확인하는 함수도 따로 존재하며, 이 때 이 완료키도 다시 받아볼 수 있다.

 

IOCP는 이벤트나 혹은 메시지등의 방법으로 결과를 통보하지 않는다.

-> 결과에 대한 정보를 IOCP 내부에서 큐로 저장을 하며 우리가 순차적으로 저장된 큐에서 정보를 받아온다.

-> 언제 IO가 완료될지 모르므로 항상 이 큐를 감시해야한다.

 

 

Completion Port 커널 오브젝트 생성하기.

IOCP 객체도 커널 오브젝트라고했다. 즉 OS로부터 새롭게 할당 받아야 하는 OS의 자원이란 의미.

-> 참고로 커널 오브젝트의 생성을 요청하면 커널모드로 돌아가 커널 오브젝트를 생성하고, 우리의 프로세스는

준비상태(프로세스의 4가지 상태)로 돌아가 스케쥴러에게 다시 선택받기를 기다려야함.

 --> 우리가 사용하는 일반 프로그램은 유저모드(유저 프로세스), 운영체제도 프로그램이기에 프로세스지만, 하드웨어와 우리의 프로세스 사이에서 인터페이스 역할을 하므로 커널단에서 돌아감(커널모드, 커널 프로세스)

 

 IOCP 객체 생성을 요청하는 API함수는 밑과 같다.

이 함수를 사용하는 방법이 2가지가 있다.

 

1. 새로운 IOCP 객체를 생성한다.

::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);

2. 기존의 IOCP 객체에 새로운 입출력 핸들을 등록한다.

handleIOCP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
::CreateIoCompletionPort(socketHandle, hanmdleIOCP, (DWORD)CompletionKey, 0);

각 인자가 의미하는 것

 

- HANDLE fileHandle 

 -> 중첩 I/O로 생선된 핸들 값. 만약 이 값이 INVALID_HANDLE_VALUE라면 새로운 IOCP 객체를 만든다

- HANDLE ExistingCompletionPort

 -> 이 전에 이미 생성된 IOCP를 넣는 곳, IOCP객체가 존재해서 넘긴다면 첫번째 인자로 넘긴 핸들을 IOCP에등록

 -> 만약 NULL을 넘기면 함수는 새로운 IOCP를 생성.

- DWORD CompletionKey

 -> fileHandle에서 발생하는 입출력 작업이 완료되었을 때 이 값을 사용자에게 반환한다.

- DWORD NumberOfConcurrentThreads

 -> 새로운 IOCP생성시에 그 포트에 동시에 접근되는 쓰레드의 허용개수를 의미

 -> 이 값을 0으로 입력하면 시스템은 CPU의 개수만큼을 할당하게 된다.

 

완료 이벤트 얻어오기

- IOCP 커널오브젝트는 입출력 작업을 완료한 후에 결과를 큐에 쌓아둔다.

- 새로운 IOCP 커널오브젝트를 생성해쏘 작업 핸들을 등록했다면 이제는 완료가 일어나는지 검사를 해야한다.

- 이 작업을 수행하는 함수가 바로 GetQueuedCompletionStatus

 

 GetQueuedCompletionStatus 함수의 원형은 다음과 같다.

각 인자가 의미하는 것

 

- HANDLE CompletionPort

 -> CreateIoCompletionPort 함수에 의해 생성된 IOCP 커널오브젝트의 핸들 값.

- LPDWORD lpNumberOfBytesTransferred

 -> 입출력에서 작업된 바이트 수를 반환한다

- LPDWORD lpCompletionKey

 -> 해당 핸들과 함께 등록된 완료키 값을 반환한다

 -> 우리가 정의한 구조체를 넘겼다면 그대로 반환. (다시 형변환하여 꺼내쓰면 된다)

- LPOVERLAPPED lpOverlapped 

 -> 해당 입출력 작업이 시작될 때 함께 입력된 OVERLAPPED 구조체의 포인터를 반환한다

 -> 이거또한 완료키처럼 OVERLAPPED의 구조체를 추가하여 필요한 정보(버퍼등)를 추가해서 쓸 수 있다

 -> 그대로반환하므로

- DWORD dwMilliseconds

 -> 타임아웃 값. (큐 버퍼가 비었을 경우 언제까지 대기할건지)

 -> INFINITE를 넣어주면 위 함수는 완료 큐에 정보가 들어올 때까지 무한정 대기.

 -> 하지만 INFINITE를 넣어주면 해당 작업 쓰레드에서 다른 작업을 진행하는데 문제가 발생하기에 보통 0~1ms정도

     넣어주어 작업 루프를 계속 회전시키는 방법을 택한다.

 

반환값은 완료큐에서 이벤트를 얻어오면 0이 아닌값(TRUE) 얻어오지못하면(TimeOut등) 0(FALSE)을 반환.

 

참고로 IOCP에 등록된 핸들은 임의로 사용자가 핸들이나 소켓등을 종료시켜도 IOCP에서 자동적으로 제거해준다.

 

 

네트워크와 IOCP

소켓을 IOCP에 등록하는건 위에서 한거랑 다른게 없다. (단 비동기 소켓 - OVERLAPPED로 생성되어야함)

즉 위에서 말한 등록방법을 그대로 하면된다.

hClntSock = ::accept(listenSocket, (SOCKADDR*)&connectClntAddr, sizeof(SOCKADDR));
::CreateIoCompletionPort(hClntSock, hIOCP, (DWORD)hClntSock, 0);

위에서 IOCP 객체에 완료 키(Completion Key)를 바로 소켓으로 넘겨주었는데,

보통은 작업 완료 시에 필요한 정보가 들어있는 구조체나(소켓을 포함한 필요한 정보들) 클래스등의 포인터를 사용하는게 일반적.

 

열려있는 소켓을 통해 상대방과 네트워킹(데이터를 주고받는)할 수 있도록 기능을 지원하는 API들

- send

- sendto

- WSASend

- WriteFile

여기서 WriteFile을 사용해본다.

- HANDLE hFile

 -> 쓰기 작업에 사용할 목적파일 핸들 값

- LPCVOID lpBuffer

 -> hFile 핸들로 지정된 파일에 보낼 버퍼의 포인터.

- DWORD nNumberOfBytesToWrite

 -> lpBuffer에서 저장할 용량을 바이트 단위로 입력한다.

- LPDWORD lpNumberOfBytesWritten

 -> 쓰기 작업에서 실제로 쓰기가 된 용량을 반환

- LPOVERLAPPED lpOverlapped

 -> OVERLAPPED 구조체의 포인터값. 이 구조체는 hFile이 FILE_FLAG_OVERLAPPED로 생성되었을 때 필요.

 -> 이 구조체는 GetQueuedCompletionStatus 함수에서 Overlapped의 인자로 받아올 수 있다.

 

패킷 송신

 

여기서 하나의 소켓에 대해서 입력과 출력 두가지 작업을 할건데, 일반적으로 GetQueuedCompletionStatus가 반환하는 정보만으로는 이게 입력의 작업인지 출력의 작업인지 판단할 수가 없다.

-> ex) 클라이언트로부터 소켓을 통해 패킷을 받았는데 완료 결과로 봐서는 그 작업이 어떤 작업인지 판단하지못하면

아무것도 할 수 없음.

 

때문에 이 부분을 해결할 수 있는 방법중 한가지.

struct OVERLAPPEDEX : OVERLAPPED
{
	DWORD m_flag;
};

(기존의 OVERLAPPED 구조체를 확장하여 flag추가)

(기존의 OVERLAPPED를 상속받았으므로 부모로 형변환 가능 -> 업캐스팅)

#define ASYNCFLAG_SEND 0x01 
#define ASYNCFLAG_RECV 0x02

struct OVERLAPPEDEX : public OVERLAPPED
{
	DWORD m_flag;
};

.....
DWORD writtenBytes = 0;

OVERLAPPEDEX overlappedSend;
::ZeroMemory(&overlappedSend, sizeof(OVERLAPPEDEX));
overlappedSend.m_flag = ASYNCFLAG_SEND;

if (::WriteFile((HANDLE)socketValue, writeBuffer, writeSize, &writtenBytes, (LPOVERLAPPED)&overlappedSend))
{
	// true면 에러체크 ::GetLastError()
}

중요한것. WriteFile의 마지막으로 넘겨준 Overlapped구조체는 GetQueued함수를 통해서 다시 얻어올 수 있다.

 -> 하지만 잘 생각해야하는 것. 위에서 지역변수로 넘겨주고 있는데 만약 결과확인을 이 함수가 아닌 다른 곳에서한다면 GetQueued로 얻어올때 쓰레기값으로 얻어올 수 있음. 

-> 그래서 보통은 Overlapped를 사용할 때 전역 혹은 동적할당,멤버변수등으로 사용하자.

 

패킷 수신

- 패킷 수신의 경우는 송신과는 성격이 조금 다르다.

 -> 왜냐하면 수신 이벤트가 언제 발생할지 알 수 없기때문.

 

이것은 IOCP가 비동기 작업이라는 점에 착안을 해보면 해결책이 나온다.

-> 언제 수신되는지 모르기 때문에 소켓이 생성된 직후에 수신을 대기하고 있으면 된다.

즉 소켓에 대해서 수신을 요청하면 IOCP는 수신 작업이 완료될때까지 항상 대기하고 있는다.

-> 하지만 아직 수신 작업이 시작되지 않았으므로 수신 이벤트를 기다리고 있다가 클라이언트로부터 수신이 이루어지면

    수신 이벤트를 처리하고 완료 통보한다.

 

 

수신을 할때는 WriteFile과 같은 ReadFile이라는 함수를 사용한다.

WriteFile과 다른게없다.

단 2,3바이트는 각 쓰는게 아닌 읽어오는 것. (읽어와 저장할 버퍼의 포인터,읽어 들일 크기)

DWORD readBytes = 0; // 읽어들인 바이트
DWORD bufSize = 8192; // 읽어들일 버퍼의 최대값

OVERLAPPEDEX overlappedRecv;
::ZeroMemory(&overlappedRecv, sizeof(OVERLAPPEDEX));
overlappedRecv.m_flag = ASYNCFLAG_RECV;

if (::ReadFile((HANDLE)socketValue, recvBuffer, bufSize, &readBytes, (LPOVERLAPPED)&overlappedRecv))
{
	// true면 에러체크 ::GetLastError()
}

 

Worker Thread

- 이제 완료 큐의 처리방법을 알아본다.

- 작업이 끝난 후에 결과는 완료 큐에 쌓이며, 우리는 이 완료 큐를 IOCP에 대한 작업이 더이상 발생하지 않을때까지 감시를 해야한다.

- 이 작업은 메인 프로세스안에서 처리할 수 있지만 네트워킹과 같이 입출력작업이 매우 빈번히 발생하는 경우에는 메인프로세스의 루프만으로는 처리하기가 힘들다 -> 쓰레드의 사용이유

 

쓰레드 풀링(Thread Pooling)

- 어떤 정보를 처리하기 위해서는 그 작업을 전담하는 객체가 필요.

- 그리고 위의 객체를 관리하는 방법에 따라 작업 효율이 크게 변한다 

 -> 동적할당과 정적할당의 차이에서 비롯됨.

 

동적 할당의 방법

- 객체가 필요한 순간마다 필요한 만큼 생성을하고 사용이 끝난 뒤에는 제거를 하는 작업을 말함

- 이 경우 객체가 차지하는 메모리의 크기가 클수록 할당하는데 걸리는 시간이 길기에 빈번히 발생하는 작업에는 적절치못함.

 

정적 할당의 방법

- 필요한 객체를 전역으로 미리 만들어 두고 필요한 순간에 접근하여 사용하는 방법.

- 때문에 빈번히 접근하거나 혹은 여러 코드에서 공통된 접근이 필요한 경우 사용한다.

- 하지만 위의 경우가 아니라면 오히려 메모리의 낭비를 초래하기에 신중하게 생각해야한다.

 

우리는 IOCP를 네트워킹에 응용하여 사용할것이다.

네트워킹의 입출력작업은 매우 빈번히 일어나 메인 쓰레드만으로는 처리하기 힘들다.

-> 그렇기때문에 이 작업만을 전담할 쓰레드를 따로 두어 입출력 작업을 보다 빠르게 처리해야 한다.

-> 그렇다고 매번 입출력 작업이 발생할때마다 이 작업을 전담할 쓰레드를 생성하면 그 부하는 무시못한다.

 --> 위와같이 처리해버리면 오히려 메인 쓰레드만으로 처리하는거보다 못한 결과가 나옴.

-> 때문에 빈번히 발생하게 될 네트워킹 작업을 위해 우리는 전담 작업 쓰레드를 생성해야한다.

 

이렇게 작업 쓰레드를 따로 두게되면 메인 스레드와는 별도로(병렬로) 진행되기에 효율이 높아진다.

-> 완료큐에 대한 전담 작업 쓰레드도 하나가아닌 여러개를 두어 사용

-> 하나만 두고 사용할 경우 입출력작업이 많아져 그 완료 이벤트는 완료큐에 쌓여만간다.

-> 여러 작업 쓰레드는 완료큐에 정보가 들어왔을 때 대기중인 쓰레드 중 하나가 먼저 작업을 시작

-> 나머지 쓰레드는 대기

 

이렇듯 쓰레드를 미리 생성해두고 활용하는 방법을 쓰레드 풀링(Thread Pooling)이라 한다.

 

Worker Thread의 사용 예)

1. 생성

HANDLE hIOCP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
...
::CreateIoCompletionPort((HANDLE)socketValue, hIOCP, socketValue, 0);
...
for (int i = 0; i < 4; ++i)
{
	workerThread[i] = new WorkerThread();
	workerThread[i]->begin();
}

2. 완료큐 확인 전담 작업 쓰레드

void WorkerThread::Run()
{
	// 입출력작업된 바이트 수/ 완료 키(보통은 구조체,클래스로 한번 래핑하여 사용)
	// 얻어오기 위해.
	DWORD bytesTransfer, keyValue; 
	LPOVERLAPPEDEX overlapped;
	BOOL retVal;

	while (m_processLoop == true)
	{
		// 매번초기화. 이전에 확인되었던 완료이벤트값이 남겨져있는것을 무시하기 위해
		keyValue = 0;
		overlapped = 0;
		retVal = ::GetQueuedCompletionStatus(hIOCP, &bytesTransfer,
			&keyValue, &overlapped, 1);
		// bytesTrasnfer는 입출력이 완료된 이벤트가 있었다면 0이상의 값임
		if (retVal != FALSE && keyValue != 0 && bytesTransfer != 0 && overlapped != 0)
		{
			if(overlapped.flag == ASYNCFLAG_SEND)
				..... 패킷 보내기 완료
			else
				..... 패킷 수신 처리
		}
	}
}

 

 

예제)

#pragma once

#include "std.h"

#define ASYNCFLAG_READ 0x00
#define ASYNCFLAG_WRITE 0x0

#define READBUFFER_SIZE 1024
#define WORKERTHREAD_COUNT 5

class Entry
{
public:
	Entry();
	virtual ~Entry();
public:
	bool Initialize();
	void Begin();
private:
	struct OVERLAPPEDEX : public OVERLAPPED
	{
		DWORD m_flag;
	};
private:
	static DWORD WorkerThread(LPVOID parameter); // static함수인 이유 -> CreateThread 함수주소 넘기기위해
	bool		 WaitForRead();
	bool		 DispatchRead(DWORD transferred);
	bool		 DispatchWrite(DWORD transferred);
private:
	HANDLE		 m_hIOCP; // IOCP 커널오브젝트
	HANDLE		 m_workerThread[WORKERTHREAD_COUNT]; // 완료큐 전담작업 쓰레드
	HANDLE		 m_fileRead; // 읽기파일 핸들
	HANDLE		 m_fileWrite; // 쓰기파일 핸들
	HANDLE       m_eventKillThread; // 이벤트 커널 오브젝트(작업완료시 쓰레드 종료위해)
	OVERLAPPEDEX m_overlappedRead; // 읽기용 overlapped구조체
	OVERLAPPEDEX m_overlappedWrite; // 쓰기용 overlapped구조체
	char		 m_readBuffer[READBUFFER_SIZE]; // 읽기 버퍼. 읽어온 데이터 저장함.
};
#include "Entry.h"

Entry::Entry() : m_fileRead(INVALID_HANDLE_VALUE), m_fileWrite(INVALID_HANDLE_VALUE),
				 m_hIOCP(INVALID_HANDLE_VALUE),m_eventKillThread(INVALID_HANDLE_VALUE)
{
	for (int i = 0; i < WORKERTHREAD_COUNT; ++i)
		m_workerThread[i] = INVALID_HANDLE_VALUE;
}


Entry::~Entry()
{
	//  작업에 사용된 모든 핸들을 종료 시킨다.
	SAFE_CLOSEHANDLE(m_fileRead);
	SAFE_CLOSEHANDLE(m_fileWrite);
	SAFE_CLOSEHANDLE(m_hIOCP);
	SAFE_CLOSEHANDLE(m_eventKillThread);
}

bool Entry::Initialize()
{
	{
		// IOCP를 사용하기 위해서는 비동기 입출력 핸들로 생성해야한다.
		m_fileRead = ::CreateFile((L"TestFile.dat"), GENERIC_READ, 0, 0, OPEN_EXISTING,
			FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,0);
		if (m_fileRead == INVALID_HANDLE_VALUE)
			return false;
		// IOCP를 사용하기 위해서는 비동기 입출력 핸들로 생성해야한다.
		m_fileWrite = ::CreateFile((L"IOCPOutput.dat"), GENERIC_WRITE, 0, 0, CREATE_ALWAYS,
			FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,0);
		if (m_fileWrite == INVALID_HANDLE_VALUE)
			return false;
	}

	{
		//IOCP생성
		m_hIOCP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
		if (m_hIOCP == INVALID_HANDLE_VALUE)
			return false;

		// Worker Thread 생성
		DWORD threadID;
		int i;
		for (i = 0; i < WORKERTHREAD_COUNT; ++i)
			m_workerThread[i] = ::CreateThread(0, 0, WorkerThread, this, 0, &threadID);

		// Thread Kill Event 생성
		m_eventKillThread = ::CreateEvent(0, TRUE, FALSE, 0); // Manual-reset모드,이벤트 커널오브젝트 non-signaled상태.
	}

	// IOCP에 핸들 등록하기
	{
		::CreateIoCompletionPort(m_fileRead, m_hIOCP, (DWORD)m_fileRead, 0);
		::CreateIoCompletionPort(m_fileWrite, m_hIOCP, (DWORD)m_fileWrite, 0);
	}
	return true;
}

void Entry::Begin()
{
	//  구조체를 초기화 한다.
	::ZeroMemory(&m_overlappedWrite, sizeof(m_overlappedWrite));
	::ZeroMemory(&m_overlappedRead, sizeof(m_overlappedRead));

	m_overlappedRead.m_flag = ASYNCFLAG_READ;
	m_overlappedWrite.m_flag = ASYNCFLAG_WRITE;

	WaitForRead();

	// 모든 Worker Thread가 종료될때까지 기다린다.
	while (::WaitForMultipleObjects(WORKERTHREAD_COUNT, m_workerThread, TRUE, INFINITE));
}

DWORD Entry::WorkerThread(LPVOID parameter)
{
	Entry* entry =  (Entry*)parameter;
	HANDLE	handleIOCP = entry->m_hIOCP;
	DWORD	bytesTransfer, keyValue;
	OVERLAPPEDEX* overlapped;
	BOOL retVal;
	while (true)
	{
        // Event커널오브젝트가 Signaled면 쓰레드는 반복문을 빠져나와 함수종료.
		if (::WaitForSingleObject(entry->m_eventKillThread, 1) == WAIT_OBJECT_0)
			break;

		keyValue = 0;
		overlapped = 0;
		retVal = ::GetQueuedCompletionStatus(
			entry->m_hIOCP, &bytesTransfer, (PULONG_PTR)&keyValue, (LPOVERLAPPED*)&overlapped, 1);
		

		if (retVal == TRUE && keyValue != 0 && bytesTransfer != 0 && overlapped != 0)
		{
			//들어오면 완료큐에서 뽑아와 데이터 확인.
			// 완료된 작업이 Read인지 Write인지 판단하는 기준은 OverlappedEx의 Flag.
			if (overlapped->m_flag == ASYNCFLAG_READ)
				entry->DispatchRead(bytesTransfer);
			else
				entry->DispatchWrite(bytesTransfer);
		}
	}

	return 0;
}

bool Entry::WaitForRead()
{
	DWORD readBytes = 0;
	DWORD bufSize = READBUFFER_SIZE;

	if (::ReadFile(m_fileRead, m_readBuffer, bufSize, &readBytes, (LPOVERLAPPED)&m_overlappedRead) == FALSE)
	{
		DWORD lastError = ::GetLastError();

		if (lastError != ERROR_IO_PENDING && lastError != ERROR_SUCCESS)
			return false;
	}
	return true;
}

bool Entry::DispatchRead(DWORD transferred)
{
	std::cout << transferred << (L"Bytes Read.") << std::endl;

	// Offset은 파일내부에서의 전송이 시작되어야 할 위치.
	// 이 위치는 파일의 시작점으로부터의 바이트 단위 Offset.
	// OVERLAPPED구조첼 사용하는 프로세스가 ReadFile,WriteFile을 호출하기전에 이 값을 세팅해야함.
	m_overlappedRead.Offset += transferred;

	// 읽어들인 내용을 저장한다.
	{
		DWORD writtenBytes = 0;

		if (::WriteFile(m_fileWrite, m_readBuffer, transferred, &writtenBytes, (LPOVERLAPPED)&m_overlappedWrite) == FALSE)
		{
			DWORD lastError = ::GetLastError();

			if (lastError != ERROR_IO_PENDING && lastError != ERROR_SUCCESS)
			{
				std::cout << (L"Error occured in write file!!") << std::endl;

				return false;
			}
		}

		m_overlappedWrite.Offset += writtenBytes;
	}

	//  읽기 작업을 새로 시작한다.
	if (WaitForRead() == false)
	{
		//  읽기에 실패한 경우는 파일을 끝까지 읽었을 때이므로, 작업을 종료시킨다.
		::SetEvent(m_eventKillThread); // 이벤트 커널오브젝트를 Signaled상태로 변경.
									   // 즉 작업을 종료시킨다 그 의미.
		return false;
	}
	return true;
}

bool Entry::DispatchWrite(DWORD transferred)
{
	std::cout << transferred << (L" bytes wrote.") << std::endl;
	return true;
}

 

main.cpp

#include "Entry.h"
int main()
{
    Entry entry;
    if (entry.Initialize() == false)
    {
        std::cout << "Failed to Initialize Program" << std::endl;
        exit(0);
    }

    DWORD startTick = ::GetTickCount(); // 작업쓰레드 수의 차이에 얼마나 속도가 차이나는지 체크.
    entry.Begin();

    std::cout << "Using " << WORKERTHREAD_COUNT << "Worker thread" << std::endl;
    std::cout << ::GetTickCount() - startTick << " millisecond passed" << std::endl;
}

 

 

반응형
Comments