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 APIC/C++ Runtime Library
CRT 초기화직접 하지 않음수행함
반환값HANDLEuintptr_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 count
count = 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. 스레드 종료

종료 방법:

  1. 스레드 함수가 return
  2. ExitThread 호출
  3. _endthread / _endthreadex 호출
  4. 프로세스 종료
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

  1. errno, strtok, printf 내부 버퍼 같은 CRT 기능들은 내부적으로 “이 스레드만의 저장 공간(TLS, Thread Local Storage)“을 사용. 해당 공간이 있냐 없냐의 차이로 보아도 될 것 같다.