기록
240113 [IOCP] thread 작동 방식 이해하기
hayo_su
2024. 1. 13. 16:09
짚고 가야 할 3가지 함수
1. CreateIoCompletionPort
I/O(입출력) 완료 포트를 만들고 지정된 파일 핸들에 연결한다.
파일 핸들과 관련된 비동기 I/O 작업 완료 알림을 받을 수 있다.
2. GetQueuedCompletionStatus
I/O(task)가 완료된 패킷을 큐에서 빼는 상태다.
3. PostQueuedCompletionStatus
I/O(task)완료 패킷에 할 일을 담아 큐에 다시 넣는 작업이다
처음 사용할 쓰레드를 포스트해주면 -(없는)일을 마치고 대기상태가 된다.
전체 실행 흐름으로 작성하였다.
thread_local int Value = 0;
int ResultValue = 0;
std::atomic_int ThreadIndex = 0;
std::mutex Lock;
HANDLE IOCPHandle = 0;
int main()
{
// IOCP 핸드를 받아 그 개수만큼 쓰레드를 사용하겠다는 것
IOCPHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 30);
// 포트 여는데 실패하는 경우
if (nullptr == IOCPHandle)
{
int a = 0;
return 1;
}
// 30개의 쓰레드를 만들겠다 이말이야
std::vector<std::thread> AllThread;
for (size_t i = 0; i < 30; i++)
{
//이 Thread는 test라는 함수를 실행한다.
AllThread.push_back(std::thread(Test));
}
IOCP 포트를 열어 핸들러를 할당받고, 쓰레드를 생성하여 이 쓰레드가 Test함수포인터를 실행하도록 한다.
void Test()
{
std::wstring Text = L"IOCP_Work_Thread ";
// 쓰레드 인덱스 설정
Text += std::to_wstring(++ThreadIndex);
// 쓰레드이름 설정
SetThreadDescription(GetCurrentThread(), Text.c_str());
DWORD Byte;
// I/O 작업이 완료된 파일 핸들과 연결된 완료 키 값을 받는 변수에 대한 포인터입니다.
ULONG_PTR PtrKey;
// 완료된 I/O 작업이 시작될 때 지정된 OVERLAPPED 구조체의 주소를 수신하는 변수에 대한 포인터
LPOVERLAPPED OverLapped = nullptr;
while (true)
{
// 윈도우가 강제로 이 쓰레드를 정지시켜준것.
// I/O 완료 패킷을 지정된 I/O 완료 포트의 큐에서 제거하려고 시도합니다.
// 대기 중인 완료 패킷이 없으면 함수는 완료 포트와 연결된 보류 중인 I / O 작업이 완료될 때까지 기다립니다.
GetQueuedCompletionStatus(IOCPHandle, &Byte, &PtrKey, &OverLapped, INFINITE);
// GetQueuedCompletionStatusEx <= 이것은 좀더 상위개념으로 여러 완료 포트 항목을 동시에 검색합니다.
// 지정된 완료 포트와 연결된 보류 중인 I/O 작업이 완료될 때까지 기다립니다.
ThreadWork WorkType = static_cast<ThreadWork>(Byte);
if (WorkType == ThreadWork::Destroy)
{
break;
}
// 쓰레드가 해당 일을 할 수 있도록 한다. - 일처리
if (WorkType == ThreadWork::Job)
{
Job* JobPtr = reinterpret_cast<Job*>(PtrKey);
if (nullptr != JobPtr->Function)
{
JobPtr->Function();
}
delete JobPtr;
}
// 진짜 일이 있으면 깨어나게 만드는 것이다.
// 윈도우는 이걸 하기위해서 Iocp라고 하는 쓰레드 핸들링 인터페이스를 지원합니다.
// 일이 없는거야.
// 멈춘다 => 일이 생길때까지
}
}
GetQueuedCompletionStatus 를 통해 핸들러에서 쉬고있는 쓰레드를 가져다 일을 할당한다.
while (true)
{
// 서버로부터 패킷을 받았을 때(input을 받았을 때)
int Select = _getch();
Job* NewJob = new Job();
// input이 어떤것인지 확인하고
switch (Select)
{
case 'a':
case 'A':
{
std::string Path = "aaaaaaaaaaaaaaaaaaaaaaa";
NewJob->Function = std::bind(TextureLoading, Path);
break;
}
case 's':
case 'S':
NewJob->Function = SoundLoading;
break;
default:
break;
}
// I / O 완료 패킷을 I / O 완료 포트에 게시
PostQueuedCompletionStatus(IOCPHandle, static_cast<DWORD>(ThreadWork::Job), reinterpret_cast<unsigned __int64>(NewJob), nullptr);
}
PostQueuedCompletionStatus를 사용하여 쓰레드에 일을 할당한다.
_getch() 대신 비동기식으로 하면 imgui 채팅프로젝트에서 사용할 수 있지 않을까!? 고민해보는 중이다.
다 끝나면 쓰레드 종료!
for (size_t i = 0; i < AllThread.size(); i++)
{
AllThread[i].join();
}
코드 한번에 보기
#include <iostream>
#include <string>
#include <Windows.h>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>
#include <conio.h>
#include <functional>
void TextureLoading(std::string _Path)
{
// aaa.png
// bbb.png
// ccc.png
std::cout << _Path << std::endl;
}
void SoundLoading()
{
std::cout << "사운드를 로딩합니다" << std::endl;
}
// 왜 iocp ?? window에서 지원해주니까!!
//
//
// 이건 쓰레드마다 생기는 전역 변수가 됩니다.
// 17부터 사용이 가능하다.
thread_local int Value = 0;
int ResultValue = 0;
std::atomic_int ThreadIndex = 0;
std::mutex Lock;
HANDLE IOCPHandle = 0;
class Job
{
public:
std::function<void()> Function;
};
enum class ThreadWork
{
Job,
Destroy,
};
void Test()
{
std::wstring Text = L"IOCP_Work_Thread ";
Text += std::to_wstring(++ThreadIndex);
// 쓰레드이름 설정
SetThreadDescription(GetCurrentThread(), Text.c_str());
DWORD Byte;
// I/O 작업이 완료된 파일 핸들과 연결된 완료 키 값을 받는 변수에 대한 포인터입니다.
ULONG_PTR PtrKey;
// 완료된 I/O 작업이 시작될 때 지정된 OVERLAPPED 구조체의 주소를 수신하는 변수에 대한 포인터
LPOVERLAPPED OverLapped = nullptr;
while (true)
{
// 윈도우가 강제로 이 쓰레드를 정지시켜준것.
// I/O 완료 패킷을 지정된 I/O 완료 포트의 큐에서 제거하려고 시도합니다.
// 대기 중인 완료 패킷이 없으면 함수는 완료 포트와 연결된 보류 중인 I / O 작업이 완료될 때까지 기다립니다.
GetQueuedCompletionStatus(IOCPHandle, &Byte, &PtrKey, &OverLapped, INFINITE);
// GetQueuedCompletionStatusEx <= 이것은 좀더 상위개념으로 여러 완료 포트 항목을 동시에 검색합니다.
// 지정된 완료 포트와 연결된 보류 중인 I/O 작업이 완료될 때까지 기다립니다.
ThreadWork WorkType = static_cast<ThreadWork>(Byte);
if (WorkType == ThreadWork::Destroy)
{
break;
}
// 쓰레드가 해당 일을 할 수 있도록 한다. - 일처리
if (WorkType == ThreadWork::Job)
{
Job* JobPtr = reinterpret_cast<Job*>(PtrKey);
if (nullptr != JobPtr->Function)
{
JobPtr->Function();
}
delete JobPtr;
}
// 진짜 일이 있으면 깨어나게 만드는 것이다.
// 윈도우는 이걸 하기위해서 Iocp라고 하는 쓰레드 핸들링 인터페이스를 지원합니다.
// 일이 없는거야.
// 멈춘다 => 일이 생길때까지
}
}
int main()
{
// CreateIoCompletionPort는 2가지 용도로 사용되는데 윈도우에게 IOCP기능을 이용하겠다고 하고
// IOCP핸드를 받는 용도가 첫번째.
// 그 개수만큼의 쓰레드 관리를 하겠다고 하는것.
// 2번째 용도는 특정 통신회전을 담당하는 핸들을 관리해달라? => 서버할때
// 2번째 용도는 우리에게 의미가 없다.
IOCPHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 30);
if (nullptr == IOCPHandle)
{
int a = 0;
return 1;
}
std::vector<std::thread> AllThread;
for (size_t i = 0; i < 30; i++)
{
AllThread.push_back(std::thread(Test));
}
while (true)
{
// 서버로부터 패킷을 받았을 때(input을 받았을 때)
int Select = _getch();
Job* NewJob = new Job();
// input이 어떤것인지 확인하고
switch (Select)
{
case 'a':
case 'A':
{
std::string Path = "aaaaaaaaaaaaaaaaaaaaaaa";
NewJob->Function = std::bind(TextureLoading, Path);
break;
}
case 's':
case 'S':
NewJob->Function = SoundLoading;
break;
default:
break;
}
// I / O 완료 패킷을 I / O 완료 포트에 게시
PostQueuedCompletionStatus(IOCPHandle, static_cast<DWORD>(ThreadWork::Job), reinterpret_cast<unsigned __int64>(NewJob), nullptr);
}
// 쓰레드 풀링 방식
// 왜???
// 쓰레드를 만드는것 자체가 비용이 크기 때문에
// 필요할때마다 만드는 방법보다는
// 미리 많이 만들어 놓고 필요할때 깨우는 방법을 선호하기 때문이다.
for (size_t i = 0; i < AllThread.size(); i++)
{
AllThread[i].join();
}
}