1. CreateThread와 _beginthread 계열
CreateThread
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);| 인자 | 의미 |
|---|---|
| lpThreadAttributes | 보안 속성 지정 |
| dwStackSize | 스레드 스택 크기 |
| lpStartAddress | 스레드가 처음 실행할 함수 |
| lpParameter | 스레드 함수에 전달할 인자 |
| dwCreationFlags | 생성 직후 실행 여부 등 |
| lpThreadId | 생성된 스레드 ID를 받을 변수 |
- OS 수준에서 스레드를 생성
- C/C++ 런타임 라이브러리를 사용하는 코드에서는
_beginthreadex사용 권장
_beginthread / _beginthreadex
uintptr_t _beginthread(
void (__cdecl* start_address)(void*),
unsigned stack_size,
void* arglist
);
uintptr_t _beginthreadex(
void* security,
unsigned stack_size,
unsigned (__stdcall* start_address)(void*),
void* arglist,
unsigned initflag,
unsigned* thrdaddr
);- 내부적으로 Windows 스레드를 생성하되, CRT 스레드별 데이터를 먼저 초기화
- 초기화가 필요한 CRT 기능:
malloc/free,printf,errno, 일부 문자열 처리 함수, CRT 내부 버퍼
비교
| 구분 | CreateThread | _beginthread / _beginthreadex |
|---|---|---|
| 제공 주체 | Windows API | C/C++ Runtime Library |
| CRT 초기화 | 직접 하지 않음 | 수행함 |
| 반환값 | HANDLE | uintptr_t |
| 종료 방식 | ExitThread 또는 함수 반환 | _endthread, _endthreadex 또는 함수 반환 |
| C/C++ 코드에서 권장 | 주의 필요 | 일반적으로 권장 |
==1CRT 초기화==
_beginthread보다_beginthreadex가 더 권장됨. CreateThread와 형태가 유사하고 핸들 관리가 명확하기 때문.
// _beginthread: 핸들 반환 안 함, 스레드 ID 못 받음
_beginthread(ThreadFunc, 0, nullptr);
// _beginthreadex: CreateThread와 거의 동일한 구조
HANDLE h = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, nullptr, 0, &threadId);2. 스레드의 메모리 구조
프로세스 안의 스레드들은 대부분의 메모리 영역을 공유한다.
| 영역 | 공유 여부 |
|---|---|
| 코드 영역 | 공유 |
| 데이터 영역 | 공유 |
| 힙 영역 | 공유 |
| 파일 핸들 등 프로세스 자원 | 공유 |
| 스택 영역 | 스레드별 독립 |
| 레지스터 문맥 | 스레드별 독립 |
- 전역 변수, 정적 변수, 힙 객체는 모든 스레드가 접근 가능
- 지역 변수는 각 스레드의 스택에 생성되므로 독립적
3. 스레드별 독립 요소
각 스레드가 별도로 보유하는 정보:
- 스레드 ID
- 스레드 핸들
- 스택
- 레지스터 상태
- 프로그램 카운터
- 스레드 상태 정보
레지스터 상태와 프로그램 카운터는 문맥 교환 시 저장/복원된다.
4. 동시 접근 문제 (Race Condition)
- 싱글 코어: 실제 동시 실행은 없지만 스레드 전환으로 연산 중간에 끼어들 수 있음
- 멀티 코어: 여러 스레드가 실제로 동시에 실행될 수 있음
예시
++count;
// CPU 수준에서는 3단계로 분리됨
// 1. load count
// 2. add 1
// 3. store countcount = 0
Thread A: load count // 0
Thread B: load count // 0
Thread A: add 1 // 1
Thread B: add 1 // 1
Thread A: store count // 1
Thread B: store count // 1
결과: 2가 아닌 1
5. 원자성 문제
원자성: 연산이 중간에 끊기지 않고 하나의 단위로 처리되는 성질
++count는 코드상 한 줄이지만 CPU 명령어 수준에서는 여러 단계- 원자적이지 않은 연산을 여러 스레드가 수행하면 결과가 예측 불가능
해결 방법
- 임계 영역 (Critical Section)
- 뮤텍스 (Mutex)
- 세마포어 (Semaphore)
- 이벤트 (Event)
- 원자적 연산 (Atomic Operation)
- 스핀락 (Spinlock)
6. 임계 영역 (Critical Section)
여러 스레드가 동시에 접근하면 안 되는 코드 영역
EnterCriticalSection(&criticalSection);
++count;
LeaveCriticalSection(&criticalSection);- 진입 전 잠금 획득, 작업 후 잠금 해제
- 한 번에 하나의 스레드만 접근 보장
7. 스레드 상태 변화
| 상태 | 의미 |
|---|---|
| 생성 | 스레드가 만들어진 상태 |
| 준비 | CPU를 할당받기 위해 기다리는 상태 |
| 실행 | CPU를 할당받아 실행 중인 상태 |
| 대기 | 특정 이벤트나 자원을 기다리는 상태 |
| 종료 | 실행이 끝난 상태 |
- CPU 스케줄링의 실제 대상은 프로세스가 아닌 스레드
- 프로세스는 실행 환경/자원 제공, 스레드는 실제 실행 단위
8. 스레드 생성 직후 상태 제어
dwCreationFlags 값:
| 값 | 의미 |
|---|---|
| 0 | 생성 후 즉시 실행 가능 상태 |
| CREATE_SUSPENDED | 생성 후 일시 중지 상태 |
HANDLE threadHandle = CreateThread(
nullptr, 0, ThreadFunction, nullptr,
CREATE_SUSPENDED,
nullptr
);
ResumeThread(threadHandle); // 이후 실행9. SuspendThread / ResumeThread
SuspendThread(threadHandle);
ResumeThread(threadHandle);- 다른 스레드를 강제로 멈추는 방식은 위험
- 자원을 잠근 상태에서 중지되면 다른 스레드가 대기에 빠질 수 있음
- 일반적으로는 플래그, 이벤트, 조건 변수 등을 통해 스레드 스스로 안전한 지점에서 멈추도록 설계
10. 스레드 종료
종료 방법:
- 스레드 함수가
return ExitThread호출_endthread/_endthreadex호출- 프로세스 종료
DWORD WINAPI ThreadFunction(void* parameter)
{
// 작업 수행
return 0; // 가장 권장되는 방식
}- 스레드 종료 후에도 핸들은 자동으로 닫히지 않음
- 더 이상 사용하지 않으면
CloseHandle호출 필요
CloseHandle(threadHandle);11. 스레드 핸들과 커널 오브젝트
- 스레드 생성 시 OS 내부에 스레드 커널 오브젝트 생성
- 프로그램은 핸들을 통해 간접 접근
핸들 사용처:
- 스레드 종료 대기
- 우선순위 변경
- 상태 제어
- 종료 코드 확인
- 핸들 닫기
WaitForSingleObject(threadHandle, INFINITE); // 스레드 종료까지 대기12. 스레드 우선순위
SetThreadPriority(threadHandle, THREAD_PRIORITY_ABOVE_NORMAL);| 값 | 의미 |
|---|---|
| THREAD_PRIORITY_LOWEST | 가장 낮음 |
| THREAD_PRIORITY_BELOW_NORMAL | 보통보다 낮음 |
| THREAD_PRIORITY_NORMAL | 보통 |
| THREAD_PRIORITY_ABOVE_NORMAL | 보통보다 높음 |
| THREAD_PRIORITY_HIGHEST | 높음 |
| THREAD_PRIORITY_TIME_CRITICAL | 매우 높음 |
- 우선순위가 높다고 항상 먼저 실행되지는 않음 (대기 상태, I/O 상태, 시스템 정책 등 복합 고려)
- 지나치게 높은 우선순위는 다른 스레드의 CPU 할당 기회를 박탈할 수 있음
13. 스레드 장단점
장점
- 프로그램 응답성 향상
- 작업 병렬 처리
- I/O 대기 중 다른 작업 수행 가능
- 같은 프로세스 메모리 공유가 쉬움
- 프로세스 생성보다 비용이 낮음
게임 클라이언트 예시: 리소스 로딩, 파일 I/O, 네트워크 수신, 압축 해제, 비동기 작업 등을 분리 가능
단점
- 공유 자원 동기화 필요
- Race Condition 발생 가능
- 데드락 가능성
- 디버깅 난이도 증가
- 실행 순서 예측 어려움
- 문맥 교환 비용 발생
멀티스레드 버그는 항상 재현되지 않을 수 있음. 설계 단계에서 공유 자원 최소화 및 동기화 범위 명확화가 중요.
면접 질문
기초 개념
- CreateThread와 _beginthreadex의 차이는?
- CRT 초기화가 필요한 이유는?
- 스레드와 프로세스의 차이는?
- 스레드가 독립적으로 갖는 요소와 공유하는 요소는?
동기화
- Race Condition이란 무엇이며 어떻게 발생하는가?
- 원자성이란 무엇인가?
++count가 원자적이지 않은 이유는?
상태 및 제어
- 스레드의 상태 종류를 설명하라.
- CREATE_SUSPENDED 플래그는 언제 사용하는가?
- 스레드를 안전하게 종료시키는 방법은?
핸들 및 자원 관리
- 스레드 종료 후 CloseHandle을 호출해야 하는 이유는?
- WaitForSingleObject는 어떤 역할을 하는가?
Footnotes
-
errno,strtok,printf내부 버퍼 같은 CRT 기능들은 내부적으로 “이 스레드만의 저장 공간(TLS, Thread Local Storage)“을 사용. 해당 공간이 있냐 없냐의 차이로 보아도 될 것 같다. ↩