본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 19. Windows에서의 쓰레드 사용

by minjunkim.dev 2020. 8. 14.

    모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 운영체제가 생성해서 관리하는 리소스

- 프로세스, 쓰레드, 파일, 세마포어, 뮤텍스 등...

- 대부분 프로그래머의 요청에 의해서 생성되며, 요청의 방식(요청에 사용되는 함수)도 제각각.

- 리소스 관리 방식 또한 리소스의 종류에 따라 차이가 있음.

- 운영체제에 의해 생성되는 리소스들은 관리를 목적으로 정보를 기록하기 위해 내부적으로 데이터 블록을 생성함.

=> 이 데이터 블록을 가리켜 "커널 오브젝트"

 

# 커널 오브젝트의 소유자는 운영체제

- 커널 오브젝트의 생성, 관리 및 그리고 소멸시점을 결정하는 것까지 모두 운영체제의 몫.

- 커널 오브젝트는 생성의 주체도 소유의 주체도 운영체제인 데이터 블록.

 

# 프로세스와 쓰레드의 관계

- "프로그램이 시작될 때 main 함수를 호출하는 주체는 쓰레드" => main 함수의 호출주체는 쓰레드임.

- 쓰레드를 별도로 생성하지 않는 프로그램(e.g. select 기반 서버) => "단일 쓰레드 모델의 프로그램 "

- 쓰레드를 별도로 생성하는 프로그램 => "멀티 쓰레드 모델의 프로그램"

- 프로세스는 쓰레드를 담는 바구니, 실질적인 실행의 주체는 쓰레드.


# 윈도우에서의 쓰레드 생성방법

#include <windows.h>

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    SIZE_T dwSTackSize,
    LPTHREAD_START_ROUTINE lpStartADdress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId
);
// 성공시 쓰레드 핸들, 실패시 NULL 반환

- lpThreadAttributes : 쓰레드의 보안관련 정보전달, 디폴트 보안설정을 위해서 NULL 전달
- dwSTackSize : 쓰레드에게 할당할 스택의 크기를 전달, 0전달하면 디폴트 크기의 스택 생성
- lpStartADdress : 쓰레드의 main 함수정보
- lpParameter : 쓰레드의 main 함수호출 시 전달할 인자정보
- dwCreationFlags : 쓰레드 생성 이후의 행동을 결정, 0을 전달하면 생성과 동시에 실행 가능한 상태가 됨
- lpThreadId : 쓰레드 ID의 저장을 위한 변수의 주소값

 

- 이 함수가 호출되면 쓰레드가 생성되고 운영체제는 이의 관리를 위해서 커널 오브젝트도 함께 생성함.

- 그리고 마지막으로 이 커널 오브젝트의 구분자 역할을 하는, 정수로 표현되는 핸들(HANDLE)을 반환함.

- "윈도우의 핸들" == "리눅스의 파일 디스크립터"

 

# 윈도우 쓰레드의 소멸시점

- 윈도우 쓰레드의 소멸시점은 쓰레드에 의해서 처음 호출된, 쓰레드의 main 함수가 반환하는 시점이다.

- 반면, 리눅스의 쓰레드는 쓰레드의 main 함수를 반환했다고 해서 자동으로 소멸하지 않는다.
쓰레드의 소멸을 직접적으로 명시해야만 하며,

그렇지 않으면 쓰레드에 의해 할당된 메모리 공간이 계속해서 남아있게 된다.

- 리눅스 쓰레드는 pthread_join() 또는 pthread_detach() 함수 호출을 통해,

쓰레드 소멸을 유도해야 한다.

 

# 쓰레드에 안전한 C 표준함수의 호출을 위한 쓰레드 생성

- 위에서 소개한 CreateThread 함수는 C/C++ 표준함수에 대해서 안정적으로 동작하지 않음.

- 따라서, 생성된 쓰레드를 통해 C/C++ 표준함수를 호출하려면, 다음 함수를 이용해서 쓰레드를 생성해야 함.

#include <process.h>

uintptr_t _beginthreadex(
    void *security;
    unsigned stack_size,
    unsigned (*start_address)(void*);
    void *arglist,
    unsigned initflag,
    unsigned *thrdaddr
);
// 성공시 쓰레드 핸들, 실패시 0 반환

- uint_ptr_t는 64비트로 표현되는 unsigned 정수 자료형

 

- _beginthreadex 함수 이전에 정의된, 비교적 사용하기에 훨씬 편리해 보이는

_beginthread 함수가 존재하지만, 이 함수는 쓰레드 생성시 반환되는 핸들을 무효화시켜서

커널 오브젝트에 접근할 수 있는 방법을 막아버리는 문제점이 있음.

 

# Sleep 함수는 1/1000초 단위로 블로킹 상태를 만듦.

# WINAPI라는 윈도우 고유의 키워드 : 매개변수의 전달방향, 할당된 스택의 반환방법 등을 포함하는

함수의 호출규약을 명시해 놓음.

# 윈도우도 리눅스와 마찬가지로 main 함수의 반환으로 인해 프로세스가 종료되면,

그 안에 담겨있는 모든 쓰레드들도 함께 종료됨.

 

# 핸들, 커널 오브젝트, 그리고 ID의 관계

- 쓰레드 생성 => 쓰레드를 관리하기 위한 커널 오브젝트 생성 => 커널 오브젝트를 참조할 수 있는 핸들 반환

- 핸들을 통해서 커널 오브젝트 구분이 가능하고, 커널 오브젝트를 통해 쓰레드 구분이 가능하기 때문에,

결국 쓰레드의 핸들은 쓰레드를 구분하는 용도로 사용됨.

- _beginthreadex 함수의 마지막 인자를 통해 쓰레드의 ID를 얻게 되는데, "핸들의 정수값은 프로세스가 달라지면 중복될 수 있으나, 쓰레드의 ID는 프로세스의 영역을 넘어서서 중복되지 않음."

=> 쓰레드의 ID는 운영체제가 생성하는 모든 쓰레드 각각을 구분하는 용도로 사용됨.


# 커널 오브젝트는 해당 리소스의 성격에 따라 많은 정보가 담기며, 그 중에서도 프로그램의 구현에 있어서 특히 더 관심을 둬야 하는 정보에 대해 "상태(state)"라는 것을 부여함.

(e.g. 쓰레드의 커널 오브젝트에 있어서 큰 관심사는 "종료여부"

- 종료된 상태 : signaled 상태

- 종료되지 않은 상태 : non-signaled 상태)

- 프로세스의 쓰레드의 커널 오브젝트 상태가 초기에는 non-signaled 상태(초기값을 FALSE)

- 운영체제는 프로세스나 쓰레드가 종료되면,

해당 커널 오브젝트를 signaled 상태로 변경해놓음.(이벤트가 발생했으므로 TRUE)

 

# WaitForSingleObject & WaitForMultipleObjects

- 커널 오브젝트에 대해 signaled 상태인지를 확인하기 위해 호출하는 함수

#include <windows.h>

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
// 성공시 이벤트 정보, 실패시 WAIT_FAILED 반환

- hHandle : 상태확인의 대상이 되는 커널 오브젝트의 핸들

- dwMilliseconds : 1/1000초 단위로 타임아웃을 지정, 인자로 INFINITE 전달시,

커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않음

- 반환값 : signaled 상태로 인한 반환시 WAIT_OBJECT_0 반환, 타임아웃으로 인한 반환시 WAIT_TIMEOUT 반환

 

- 위 함수는 이벤트 발생에 의해서(signaled 상태가 되어서) 반환되면,

해당 커널 오브젝트를 다시 non-signaled 상태로 되돌리기도 함.(auto-reset 모드) <=> (manual-reset 모드)

 

#include <windows.h>

DWORD WaitForMultipleObjects(
DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
// 성공시 이벤트 정보, 실패시 WAIT_FAILED 반환

- nCount : 검사할 커널 오브젝트의 수

- lpHandles : (커널 오브젝트의) 핸들정보를 담고 있는 배열의 주소값

- bWaitAll : TRUE 전달시, 모든 검사대상이 signaled 상태가 되어야 반환,

FALSE 전달시, 검사대상 중 하나라도 signaled 상태가 되면 반환

- dwMilliseconds : 1/1000초 단위로 타임아웃 지정, 인자로 INFINITE 전달 시,

커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않음


# WaitForSingleObject 함수 활용 예제

#include <stdio.h>
#include <windows.h>
#include <process.h>

unsinged WINAPI ThreadFunc(void *arg); // 쓰레드의 main함수

int main(int argc, char* argv[])
{
	HANDLE hThread; // 쓰레드의 커널 오브젝트에 대한 핸들
    DWORD wr;
    unsigned threadID; // 쓰레드 ID를 저장할 변수
    int param=5; // 쓰레드 main 함수의 매개변수로 전달할 변수
    
    /* 쓰레드 생성 및 실행 */
    hThread=(HANDLE)_beginthreadex(NULL,0,ThreadFunc,(void*)&param,0,&threadID);
	if(hThread==0)
    {
    	puts("_beginthreadex() error");
        return -1;
    }
    
    if((wr=WaitForSingleObject(hThread,INFINITE))==WAIT_FAILED) // 생성한 쓰레드가 signaled 상태가 될때까지 블로킹
    {
    	puts("thread wait error");
        return -1;
    }
    
    printf("wait result:%s \n", (wr==WAIT_OBJECT_0) ? "signaled" : "time-out"); // 이벤트 정보 확인 후 출력
    puts("end of main");
    return 0;
}

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어