Windows API 멀티스레딩 | CreateThread·동기화 완벽 가이드
이 글의 핵심
Windows 멀티스레딩은 CreateThread 또는 _beginthreadex로 스레드를 생성합니다. 공유 자원은 Critical Section, Mutex, Semaphore로 보호하고, Event로 스레드 간 신호를 주고받습니다. GUI 업데이트는 메인 스레드에서만 해야 합니다.
들어가며
Windows 멀티스레딩은 1990년대 Windows NT부터 제공된 강력한 동시성 API입니다. 멀티코어 CPU 활용, 응답성 개선, 백그라운드 작업 처리에 필수적입니다. 하지만 공유 자원 접근, 데드락, 레이스 컨디션 같은 문제를 주의해야 합니다.
// 가장 간단한 스레드 생성
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
MessageBox(NULL, L"스레드 실행!", L"알림", MB_OK);
return 0;
}
HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
1. 스레드 생성
1.1 CreateThread
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
int* pValue = (int*)lpParam;
for (int i = 0; i < 5; i++) {
Sleep(1000);
(*pValue)++;
wchar_t buf[64];
swprintf_s(buf, L"값: %d", *pValue);
OutputDebugString(buf);
}
return 0;
}
int main()
{
int value = 0;
HANDLE hThread = CreateThread(
NULL, // 보안 속성
0, // 스택 크기 (0 = 기본)
ThreadFunc, // 스레드 함수
&value, // 파라미터
0, // 생성 플래그 (0 = 즉시 시작)
NULL // 스레드 ID (NULL = 받지 않음)
);
if (hThread == NULL) {
MessageBox(NULL, L"스레드 생성 실패!", L"오류", MB_ICONERROR);
return 1;
}
// 스레드 종료 대기
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
wchar_t buf[64];
swprintf_s(buf, L"최종 값: %d", value);
MessageBox(NULL, buf, L"완료", MB_OK);
return 0;
}
1.2 _beginthreadex (권장)
#include <process.h>
unsigned __stdcall ThreadFunc(void* pParam)
{
int* pValue = (int*)pParam;
for (int i = 0; i < 5; i++) {
Sleep(1000);
(*pValue)++;
}
_endthreadex(0); // 또는 return 0
return 0;
}
int main()
{
int value = 0;
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, // 보안
0, // 스택 크기
ThreadFunc, // 함수
&value, // 파라미터
0, // 플래그
NULL // 스레드 ID
);
if (hThread == 0) {
MessageBox(NULL, L"스레드 생성 실패!", L"오류", MB_ICONERROR);
return 1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
1.3 스레드 일시 정지/재개
// 일시 정지 상태로 생성
HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, NULL);
// 나중에 재개
ResumeThread(hThread);
// 또는 일시 정지
SuspendThread(hThread);
2. 스레드 동기화 - Critical Section
2.1 기본 사용법
CRITICAL_SECTION g_cs;
int g_sharedValue = 0;
DWORD WINAPI Thread1(LPVOID lpParam)
{
for (int i = 0; i < 10000; i++) {
EnterCriticalSection(&g_cs);
g_sharedValue++;
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD WINAPI Thread2(LPVOID lpParam)
{
for (int i = 0; i < 10000; i++) {
EnterCriticalSection(&g_cs);
g_sharedValue--;
LeaveCriticalSection(&g_cs);
}
return 0;
}
int main()
{
InitializeCriticalSection(&g_cs);
HANDLE threads[2];
threads[0] = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
threads[1] = CreateThread(NULL, 0, Thread2, NULL, 0, NULL);
WaitForMultipleObjects(2, threads, TRUE, INFINITE);
CloseHandle(threads[0]);
CloseHandle(threads[1]);
DeleteCriticalSection(&g_cs);
wchar_t buf[64];
swprintf_s(buf, L"최종 값: %d (0이어야 정상)", g_sharedValue);
MessageBox(NULL, buf, L"결과", MB_OK);
return 0;
}
2.2 RAII 래퍼
class CriticalSectionLock {
private:
CRITICAL_SECTION* m_pCS;
public:
CriticalSectionLock(CRITICAL_SECTION* pCS) : m_pCS(pCS) {
EnterCriticalSection(m_pCS);
}
~CriticalSectionLock() {
LeaveCriticalSection(m_pCS);
}
};
// 사용
void SomeFunction()
{
CriticalSectionLock lock(&g_cs); // 자동으로 Enter
// 크리티컬 섹션 내 작업
g_sharedValue++;
// 소멸자에서 자동으로 Leave
}
3. 스레드 동기화 - Mutex
3.1 기본 Mutex
HANDLE g_hMutex = NULL;
int g_sharedValue = 0;
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
for (int i = 0; i < 10; i++) {
// Mutex 획득 (최대 5초 대기)
DWORD result = WaitForSingleObject(g_hMutex, 5000);
if (result == WAIT_OBJECT_0) {
// Mutex 획득 성공
g_sharedValue++;
Sleep(100);
// Mutex 해제
ReleaseMutex(g_hMutex);
} else if (result == WAIT_TIMEOUT) {
MessageBox(NULL, L"Mutex 획득 타임아웃!", L"오류", MB_ICONERROR);
}
}
return 0;
}
int main()
{
g_hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
}
WaitForMultipleObjects(5, threads, TRUE, INFINITE);
for (int i = 0; i < 5; i++) {
CloseHandle(threads[i]);
}
CloseHandle(g_hMutex);
wchar_t buf[64];
swprintf_s(buf, L"최종 값: %d", g_sharedValue);
MessageBox(NULL, buf, L"결과", MB_OK);
return 0;
}
3.2 이름있는 Mutex (프로세스 간 동기화)
// 프로세스 A
HANDLE hMutex = CreateMutex(NULL, FALSE, L"MyGlobalMutex");
WaitForSingleObject(hMutex, INFINITE);
// ... 공유 자원 접근
ReleaseMutex(hMutex);
CloseHandle(hMutex);
// 프로세스 B
HANDLE hMutex = OpenMutex(SYNCHRONIZE, FALSE, L"MyGlobalMutex");
WaitForSingleObject(hMutex, INFINITE);
// ... 공유 자원 접근
ReleaseMutex(hMutex);
CloseHandle(hMutex);
4. 스레드 동기화 - Semaphore
4.1 기본 Semaphore
HANDLE g_hSemaphore = NULL;
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int id = (int)(INT_PTR)lpParam;
// Semaphore 획득 시도
WaitForSingleObject(g_hSemaphore, INFINITE);
wchar_t buf[128];
swprintf_s(buf, L"Thread %d: 작업 시작", id);
OutputDebugString(buf);
Sleep(2000); // 작업 시뮬레이션
swprintf_s(buf, L"Thread %d: 작업 완료", id);
OutputDebugString(buf);
// Semaphore 해제
ReleaseSemaphore(g_hSemaphore, 1, NULL);
return 0;
}
int main()
{
// 최대 3개 스레드만 동시 실행 가능
g_hSemaphore = CreateSemaphore(
NULL, // 보안
3, // 초기 카운트
3, // 최대 카운트
NULL // 이름
);
// 10개 스레드 생성
HANDLE threads[10];
for (int i = 0; i < 10; i++) {
threads[i] = CreateThread(NULL, 0, WorkerThread, (LPVOID)(INT_PTR)i, 0, NULL);
}
WaitForMultipleObjects(10, threads, TRUE, INFINITE);
for (int i = 0; i < 10; i++) {
CloseHandle(threads[i]);
}
CloseHandle(g_hSemaphore);
return 0;
}
5. 스레드 동기화 - Event
5.1 Manual-Reset Event
HANDLE g_hEvent = NULL;
DWORD WINAPI WaiterThread(LPVOID lpParam)
{
int id = (int)(INT_PTR)lpParam;
wchar_t buf[128];
swprintf_s(buf, L"Thread %d: 대기 중...", id);
OutputDebugString(buf);
WaitForSingleObject(g_hEvent, INFINITE);
swprintf_s(buf, L"Thread %d: 신호 받음!", id);
OutputDebugString(buf);
return 0;
}
int main()
{
// Manual-Reset Event (한 번 신호 주면 모든 스레드 깨어남)
g_hEvent = CreateEvent(
NULL, // 보안
TRUE, // Manual-Reset
FALSE, // 초기 상태 (비신호)
NULL // 이름
);
// 여러 스레드 생성
HANDLE threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = CreateThread(NULL, 0, WaiterThread, (LPVOID)(INT_PTR)i, 0, NULL);
}
Sleep(2000);
MessageBox(NULL, L"모든 스레드에 신호 전송!", L"알림", MB_OK);
// 신호 전송 (모든 대기 스레드 깨어남)
SetEvent(g_hEvent);
WaitForMultipleObjects(5, threads, TRUE, INFINITE);
for (int i = 0; i < 5; i++) {
CloseHandle(threads[i]);
}
CloseHandle(g_hEvent);
return 0;
}
5.2 Auto-Reset Event
// Auto-Reset Event (한 번에 하나의 스레드만 깨어남)
HANDLE g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // FALSE = Auto-Reset
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int id = (int)(INT_PTR)lpParam;
while (true) {
WaitForSingleObject(g_hEvent, INFINITE);
wchar_t buf[128];
swprintf_s(buf, L"Thread %d: 작업 중...", id);
OutputDebugString(buf);
Sleep(1000);
}
return 0;
}
// 메인 스레드에서 주기적으로 신호 전송
for (int i = 0; i < 10; i++) {
Sleep(500);
SetEvent(g_hEvent); // 한 스레드만 깨어남
}
6. GUI 스레드 통신
6.1 PostMessage로 진행률 업데이트
#define WM_WORKER_PROGRESS (WM_USER + 1)
#define WM_WORKER_COMPLETE (WM_USER + 2)
struct WorkerParam {
HWND hwnd;
int totalWork;
};
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
WorkerParam* pParam = (WorkerParam*)lpParam;
for (int i = 0; i <= pParam->totalWork; i++) {
// 진행률 계산
int progress = (i * 100) / pParam->totalWork;
// 메인 스레드에 진행률 전송
PostMessage(pParam->hwnd, WM_WORKER_PROGRESS, progress, 0);
Sleep(100); // 작업 시뮬레이션
}
// 완료 신호
PostMessage(pParam->hwnd, WM_WORKER_COMPLETE, 0, 0);
delete pParam;
return 0;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
static HWND hProgress = NULL;
switch (msg) {
case WM_CREATE: {
// ProgressBar 생성
hProgress = CreateWindow(
PROGRESS_CLASS, L"",
WS_CHILD | WS_VISIBLE,
10, 10, 400, 25,
hwnd, NULL, GetModuleHandle(NULL), NULL
);
SendMessage(hProgress, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
// 시작 버튼
CreateWindow(
L"BUTTON", L"시작",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
10, 50, 100, 30,
hwnd, (HMENU)1, GetModuleHandle(NULL), NULL
);
return 0;
}
case WM_COMMAND:
if (LOWORD(wParam) == 1) {
// 워커 스레드 시작
WorkerParam* pParam = new WorkerParam;
pParam->hwnd = hwnd;
pParam->totalWork = 50;
CreateThread(NULL, 0, WorkerThread, pParam, 0, NULL);
}
break;
case WM_WORKER_PROGRESS: {
// 진행률 업데이트
int progress = (int)wParam;
SendMessage(hProgress, PBM_SETPOS, progress, 0);
wchar_t buf[64];
swprintf_s(buf, L"진행률: %d%%", progress);
SetWindowText(hwnd, buf);
break;
}
case WM_WORKER_COMPLETE:
MessageBox(hwnd, L"작업 완료!", L"알림", MB_OK);
SendMessage(hProgress, PBM_SETPOS, 0, 0);
SetWindowText(hwnd, L"대기 중");
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
7. 생산자-소비자 패턴
#include <queue>
CRITICAL_SECTION g_cs;
HANDLE g_hEvent;
std::queue<int> g_queue;
bool g_bShutdown = false;
DWORD WINAPI ProducerThread(LPVOID lpParam)
{
for (int i = 0; i < 100; i++) {
EnterCriticalSection(&g_cs);
g_queue.push(i);
wchar_t buf[128];
swprintf_s(buf, L"Producer: %d 생성 (큐 크기: %zu)", i, g_queue.size());
OutputDebugString(buf);
LeaveCriticalSection(&g_cs);
// 소비자에게 신호
SetEvent(g_hEvent);
Sleep(50);
}
return 0;
}
DWORD WINAPI ConsumerThread(LPVOID lpParam)
{
int id = (int)(INT_PTR)lpParam;
while (true) {
WaitForSingleObject(g_hEvent, INFINITE);
EnterCriticalSection(&g_cs);
if (g_bShutdown && g_queue.empty()) {
LeaveCriticalSection(&g_cs);
break;
}
if (!g_queue.empty()) {
int value = g_queue.front();
g_queue.pop();
wchar_t buf[128];
swprintf_s(buf, L"Consumer %d: %d 소비 (큐 크기: %zu)",
id, value, g_queue.size());
OutputDebugString(buf);
LeaveCriticalSection(&g_cs);
Sleep(100); // 처리 시뮬레이션
} else {
LeaveCriticalSection(&g_cs);
}
}
return 0;
}
int main()
{
InitializeCriticalSection(&g_cs);
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // Auto-Reset
// 생산자 1개, 소비자 3개
HANDLE hProducer = CreateThread(NULL, 0, ProducerThread, NULL, 0, NULL);
HANDLE consumers[3];
for (int i = 0; i < 3; i++) {
consumers[i] = CreateThread(NULL, 0, ConsumerThread, (LPVOID)(INT_PTR)i, 0, NULL);
}
// 생산자 종료 대기
WaitForSingleObject(hProducer, INFINITE);
CloseHandle(hProducer);
// 소비자 종료 신호
g_bShutdown = true;
for (int i = 0; i < 3; i++) {
SetEvent(g_hEvent);
}
// 소비자 종료 대기
WaitForMultipleObjects(3, consumers, TRUE, INFINITE);
for (int i = 0; i < 3; i++) {
CloseHandle(consumers[i]);
}
CloseHandle(g_hEvent);
DeleteCriticalSection(&g_cs);
return 0;
}
8. 스레드 풀 (Thread Pool)
8.1 QueueUserWorkItem
DWORD WINAPI WorkCallback(LPVOID lpParam)
{
int id = (int)(INT_PTR)lpParam;
wchar_t buf[128];
swprintf_s(buf, L"작업 %d 실행 중 (스레드 ID: %d)", id, GetCurrentThreadId());
OutputDebugString(buf);
Sleep(2000);
swprintf_s(buf, L"작업 %d 완료", id);
OutputDebugString(buf);
return 0;
}
int main()
{
// 스레드 풀에 작업 10개 등록
for (int i = 0; i < 10; i++) {
QueueUserWorkItem(WorkCallback, (LPVOID)(INT_PTR)i, WT_EXECUTEDEFAULT);
}
// 모든 작업 완료 대기 (실제로는 다른 동기화 방법 필요)
Sleep(5000);
return 0;
}
9. 실전 예제: 멀티스레드 파일 다운로더
#include <windows.h>
#include <urlmon.h>
#pragma comment(lib, "urlmon.lib")
struct DownloadTask {
int id;
std::wstring url;
std::wstring filename;
HWND hwndProgress;
};
DWORD WINAPI DownloadThread(LPVOID lpParam)
{
DownloadTask* pTask = (DownloadTask*)lpParam;
wchar_t msg[512];
swprintf_s(msg, L"다운로드 시작: %s", pTask->url.c_str());
OutputDebugString(msg);
// 다운로드
HRESULT hr = URLDownloadToFile(
NULL,
pTask->url.c_str(),
pTask->filename.c_str(),
0,
NULL
);
if (SUCCEEDED(hr)) {
swprintf_s(msg, L"다운로드 완료: %s", pTask->filename.c_str());
OutputDebugString(msg);
SendMessage(pTask->hwndProgress, PBM_SETPOS, 100, 0);
} else {
swprintf_s(msg, L"다운로드 실패: %s (HRESULT: 0x%08X)",
pTask->url.c_str(), hr);
OutputDebugString(msg);
}
delete pTask;
return 0;
}
// 사용 예
void StartDownload(HWND hwnd, const wchar_t* url, const wchar_t* filename)
{
DownloadTask* pTask = new DownloadTask;
pTask->url = url;
pTask->filename = filename;
pTask->hwndProgress = GetDlgItem(hwnd, IDC_PROGRESS);
CreateThread(NULL, 0, DownloadThread, pTask, 0, NULL);
}
10. 데드락 방지
10.1 데드락 발생 예
// ❌ 데드락 발생 가능
CRITICAL_SECTION g_cs1, g_cs2;
DWORD WINAPI Thread1(LPVOID lpParam)
{
EnterCriticalSection(&g_cs1);
Sleep(100);
EnterCriticalSection(&g_cs2); // Thread2가 cs2 보유 중이면 데드락!
// ... 작업
LeaveCriticalSection(&g_cs2);
LeaveCriticalSection(&g_cs1);
return 0;
}
DWORD WINAPI Thread2(LPVOID lpParam)
{
EnterCriticalSection(&g_cs2);
Sleep(100);
EnterCriticalSection(&g_cs1); // Thread1이 cs1 보유 중이면 데드락!
// ... 작업
LeaveCriticalSection(&g_cs1);
LeaveCriticalSection(&g_cs2);
return 0;
}
10.2 데드락 방지
// ✅ 항상 같은 순서로 잠금 획득
DWORD WINAPI Thread1(LPVOID lpParam)
{
EnterCriticalSection(&g_cs1);
EnterCriticalSection(&g_cs2); // 순서: cs1 → cs2
// ... 작업
LeaveCriticalSection(&g_cs2);
LeaveCriticalSection(&g_cs1);
return 0;
}
DWORD WINAPI Thread2(LPVOID lpParam)
{
EnterCriticalSection(&g_cs1);
EnterCriticalSection(&g_cs2); // 순서: cs1 → cs2 (동일!)
// ... 작업
LeaveCriticalSection(&g_cs2);
LeaveCriticalSection(&g_cs1);
return 0;
}
// 또는 TryEnterCriticalSection 사용
if (TryEnterCriticalSection(&g_cs1)) {
if (TryEnterCriticalSection(&g_cs2)) {
// 두 잠금 모두 획득
// ... 작업
LeaveCriticalSection(&g_cs2);
}
LeaveCriticalSection(&g_cs1);
} else {
// 획득 실패, 다시 시도
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Windows API 기초 | 메시지 루프와 윈도우 프로시저 완벽 가이드
- Windows API 파일 I/O·레지스트리 | 파일 처리 완벽 가이드
- C++ 멀티스레딩 완벽 가이드 | thread·mutex·condition_variable
이 글이 도움이 되셨나요? Windows API 멀티스레딩을 마스터하는 데 도움이 되었기를 바랍니다!
다음 글에서는 Windows API DLL 생성과 사용을 다루겠습니다. 📚