Chapter 19 - Windows에서의 쓰레드 사용

19-1 : 커널 오브젝트(Kernel Objects)

윈도우 운영체제가 생성해서 관리하는 리소스(프로세스, 쓰레드, 파일, 세마포어, 뮤텍스 등)들은 관리를 목적으로 정보를 기록하기 위해 내부적으로 데이터 블록인 ‘커널 오브젝트’를 생성함

  • 커널 오브젝트의 소유자는 커널(운영체제)
  • 따라서 커널 오브젝트의 생성 / 관리 / 소멸은 모두 운영체제의 몫

19-2 : 윈도우 기반의 쓰레드 생성

main() 함수를 호출하는 주체는 쓰레드
현재는 운영체제 레벨에서 쓰레드를 지원
쓰레드를 별도로 생성하지 않는 프로그램을 단일 쓰레드 모델의 프로그램이라고 부름
쓰레드를 별도로 생성하는 프로그램은 멀티 쓰레드 모델의 프로그램이라고 부름


윈도우에서 쓰레드 생성에는 CreateThread() 함수를 사용

#include <windows.h>

HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
	SIZE_T dwStackSize,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	DWORD dwCreationFlags,
	LPDWORD lpThreadId
);
// 성공시 쓰레드 핸들, 실패시 NULL 반환
  • lpThreadAttributes : 쓰레드의 보안관련 정보, 디폴트 보안설정은 NULL
  • dwStackSzie : 쓰레드에게 할당할 스택 크기 전달, 디폴트 크기 설정은 0
  • lpStartAddress : 쓰레드의 main() 함수 전달
  • lpParamete : 쓰레드의 main() 함수 호출시 전달할 인자정보 전달
  • dwCreationFlags : 쓰레드 생성 이후 행동 결정, 생성과 동시에 실행 가능 상태 설정시 0
  • lpThreadId : 쓰레드 ID 저장을 위한 변수 주소값 전달

윈도우에서는 쓰레드의 main() 함수가 반환됨과 동시에 쓰레드가 소멸함


VC++에서 C/C++ 표준함수를 호출하기 위해서는 ‘C/C++ Runtime Library’를 지정해주어야함
최근 버전에서는 멀티 쓰레드 모델을 지원하는 라이브러리만 존재하기 때문에 별도의 환결설정 과정은 필요없음


CreateThread() 함수를 통해 생성되는 쓰레드는 C/C++ 표준함수에 대해 안정적으로 동작하지 않기 때문에 대신 _beginthreadex() 함수를 사용할 수 있음

#include <process.h>

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

_beginthreadex() 함수는 CreateThread() 함수와 각 매개변수가 지니는 의미 및 순서가 동일
단, 이름과 선언된 매개변수의 자료형이 다르기 때문에 적절한 형변환이 필요

윈도우에서의 쓰레드 생성 예시

// thread1_win.cpp

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

using namespace std;

void			ErrorHandling(char* message);
unsigned WINAPI	ThreadFunc(void *arg);

int	main(int argc, char *argv[])
{
	HANDLE		hThread;
	unsigned	threadID;
	int			param = 5;

	hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
	if (hThread == 0)
	{
		ErrorHandling("_beginthreadex() error");
		return -1;
	}
	Sleep(3000);
	cout << "end of main\n";
	return 0;
}

unsigned WINAPI	ThreadFunc(void *arg)
{
	int	cnt = *((int*)arg);
	for (int i = 0; i < cnt; i++)
	{
		Sleep(1000);
		cout << "running thread\n";
	}
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

WINAPI 키워드는 _beginthreadex() 함수가 요구하는 호출규약을 지키기 위해 삽입됨

핸들의 정수 값은 프로세스가 달라지면 중복될 수 있으나, 쓰레드 ID는 프로세스의 영역을 넘어서 중복되지 않음
따라서 운영체제가 생성하는 모든 쓰레드 각각을 구분하는 용도로는 쓰레드 ID를 사용


19-3 : 커널 오브젝트의 두가지 상태

커널 오브젝트에는 리소스의 성격에 따라 많은 정보가 담기고, 그중에서도 중요한 정보에 대해 상태(state)가 부여됨

  • 종료된 상태는 signaled, 종료되지 않은 상태는 non-signaled 상태라고 함
  • 위 상태는 boolean형 변수로 표현

커널 오브젝트가 signaled 상태인지 확인하기 위해서는 WaitForSingleObject() 함수 또는 WaitForMultipleObjects() 함수를 사용

#include <windows.h>

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

DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
// 성공시 이벤트 정보, 실패시 WAIT_FAILED 반환
  • hHandle : 상태확인의 대상이 되는 커널 오브젝트의 핸들 전달
  • dwMilliseconds : 1/1000초 단위로 타임아웃 지정, INFINITE 전달시 해당 오브젝트가 signaled 상태가 되기 전까지 반환하지 않음
  • bWaitAll : TRUE면 모든 검사대상이, FALSE면 하나라도 signaled 상태일 때 반환
  • signaled 상태로 인한 반환시 WAIT_OBJECT_0, 타임아웃시 WAIT_TIMEOUT 반환

signaled 상태가 된 후 다시 non_signaled 상태로 되돌려지는 커널 오브젝트를 auto-reset 모드 커널 오브젝트, 그렇지 않은 커널 오브젝트를 manual-reset 모드 커널 오브젝트라고 함

WaitForSingleObject() 함수 사용 예시

// thread2_win.cpp

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

using namespace std;

void			ErrorHandling(char* message);
unsigned WINAPI	ThreadFunc(void *arg);

int	main(int argc, char *argv[])
{
	HANDLE		hThread;
	DWORD		wr;
	unsigned	threadID;
	int			param = 5;

	hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
	if (hThread == 0)
	{
		ErrorHandling("_beginthreadex() error");
		return -1;
	}

	if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
	{
		ErrorHandling("thread wait error");
		return -1;
	}

	cout << "wait result : " << (wr == WAIT_OBJECT_0) ? "signaled" : "time-out" << endl;
	cout << "end of main\n";
	return 0;
}

unsigned WINAPI	ThreadFunc(void *arg)
{
	int	cnt = *((int*)arg);
	for (int i = 0; i < cnt; i++)
	{
		Sleep(1000);
		cout << "running thread\n";
	}
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

WaitForMultipleObjects() 함수 사용 예시

// thread3_win.cpp

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

using namespace std;

#define NUM_THREAD 50
void			ErrorHandling(char* message);
unsigned WINAPI	threadInc(void *arg);
unsigned WINAPI	threadDes(void *arg);
long long		num = 0;

int	main(int argc, char *argv[])
{
	HANDLE		tHandles[NUM_THREAD];

	cout << "sizeof long long : " << sizeof(long long) << endl;
	for (int i = 0; i < NUM_THREAD; i++)
	{
		if (i % 2)
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
		else
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
	}

	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
	cout << "result : " << num << endl;
	return 0;
}

unsigned WINAPI	threadInc(void *arg)
{
	for (int i = 0; i < 50000000; i++)
		num += 1;
	return 0;
}

unsigned WINAPI	threadDes(void *arg)
{
	for (int i = 0; i < 50000000; i++)
		num -= 1;
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

내용 확인문제

  1. 다음 중 커널 오브젝트에 대한 설명으로 옳지 않은 것을 모두 고르면?
    • 커널 오브젝트는 운영체제가 생성하는 리소스들의 정보를 저장해놓은 데이터 블록이다. (O)
    • 커널 오브젝트의 소유자는 해당 커널 오브젝트를 생성한 프로세스이다. (X)
    • 커널 오브젝트의 생성 및 관리는 사용자 프로세스에 의해서 이루어진다. (X)
    • 운영체제가 생성 및 관리하는 리소스의 종류에 상관없이 커널 오브젝트의 데이터 블록 구조는 완전히 동일하다. (X)
  2. 오늘날에는 대부분의 운영체제가 운영체제 레벨에서 쓰레드를 지원한다. 이러한 상황을 근거로 하여 다음 설명 중에서 옳지 않은 것을 모두 골라보자.
    • main 함수를 호출하는 것도 쓰레드이다. (O)
    • 프로세스가 쓰레드를 생성하지 않으면, 프로세스 내에는 쓰레드가 하나도 존재하지 않게된다. (X)
    • 멀티쓰레드 모델이란 프로세스 내에서 추가로 쓰레드를 생성하는 프로그램의 유형을 의미한다. (O)
    • 단일쓰레드 모델이란 프로세스 내에서 추가로 딱 하나의 쓰레드만 추가로 생성하는 프로그램의 유형을 의미한다. (X)
  3. 윈도우의 쓰레드를 메모리 공간에서 완전히 소멸시키는 방법과 리눅스의 쓰레드를 메모리 공간에서 완전히 소멸시키는 방법의 차이점을 비교 설명해보자.

    윈도우에서는 쓰레드의 main() 함수가 반환될시 소멸
    리눅스에서는 pthread_join() 또는 pthread_detach() 함수를 별도로 호출하여 소멸시켜야함

  4. 커널 오브젝트, 쓰레드, 그리고 핸들의 관계를 쓰레드가 생성되는 상황을 이용해서 설명해보자.

    운영체제가 쓰레드 생성시 해당 쓰레드의 정보를 담기 위한 커널 오브젝트 생성
    동시에 커널 오브젝트의 구분자에 해당하는 핸들을 반환함

  5. 커널 오브젝트와 관련된 다음 문장들 중에서 말하는 바가 옳으면 O, 틀리면 X를 표시하자.
    • 커널 오브젝트는 signaled 상태와 non-signaled 상태 중 하나의 상태가 된다. (O)
    • 커널 오브젝트가 signaled 상태가 되어야 하는 시점에 프로그래머는 직접 커널 오브젝트의 상태를 signaled 상태로 변경해야한다. (X)
    • 쓰레드의 커널 오브젝트는 쓰레드가 실행중일 때 signaled 상태에 있다가, 쓰레드가 종료되면 non-signaled 상태가 된다. (X)
  6. ‘auto-reset 모드’ 커널 오브젝트와 ‘manual-reset 모드’ 커널 오브젝트에 대해서 설명하여라. 커널 오브젝트는 어떠한 특징적 차이로 둘 중 하나로 나뉘게되는가?

    signaled 상태가 되어 WaitFor- 함수가 반환된 후 다시 자동으로 non-signaled 상태가 되는지 여부에 따라서 나뉘어짐