12.1 Windows에서의 쓰레드 생성과 소멸
12.1-1 쓰레드의 생성
Note) CreateThread() 함수
- lpThreadAttributes: 부모 쓰레드에서 자식 쓰레드에게 핸들 테이블을 상속할지 여부.
즉, 쓰레드도 별도의 자식 쓰레드를 생성할 수 있음.
- dwStackSize: 독립적인 스택의 크기를 결정
- lpStartAddress: 함수 포인터. 흔히 쓰레드 메인함수라 부름.
- lpParameter: OS가 쓰레드 메인 함수 호출 시 우리 대신 전달해줄 인자값.
- dwCreationFlags: 쓰레드 생성 직후, suspend 상태로 둘지 Blocked 상태로 둘지 결정.
메모리 매니지먼트와 관련해서 아주 중요한 매개변수가 됨. 지금은 안씀.
- lpThreadId: 이 매개변수에 적절한 인자를 전달해서, 생성되는 쓰레드의 ID를 얻을 수 있음.
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 보안관리자. 핸들 테이블의 상속 여부 결정 가능.
SIZE_T dwStackSize, // initialize stack size.
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
__drv_aliasesMem LPVOID lpParameter, // thread function's argument.
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
Note) thread function의 시그니쳐
위에는 자료형이 LPTHREAD_START_ROUTINE인데 아래는 PTHREAD_START_ROUTINE임.
근데 이거는 typedef *PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE; 이라 되어있음.
LPVOID라 함은, 원하는 자료형으로 자료형 변환해서 전달하라는 것.
근데 쓰레드의 생성은 결국 커널(os)의 할일이기 때문에, 우리가 전달할 순 없음.
OS에게 전달해 달라고 부탁해야해서, CreateThread() 함수에 4번째 매개변수로 전달.
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);
Note) Handle? ID?
핸들 테이블과 관련해서 핸들을 배웠는데, 그때를 돌이켜보면 핸들은 특정 프로세스 내에서만 유효한 값.
즉, 프로세스가 다른 커널 오브젝트를 접근하기 위해서 필요한 개념.
근데 ID는 좀 더 광범위한 개념임. 프로세스 1, 프로세스 2, 프로세스 3이 있다고 가정 해 보자.
각각의 프로세스 내에서 임의의 개수로 쓰레드를 생성했을 때, 해당 쓰레드의 ID는 고유함.
즉, OS의 범주에서까지 고유한 값으로써의 개념임. 어떨때는 핸들 개념을 써야할 때가 있고
어떨때는 ID 개념을 써야할 때도 있음. 이 책에서는 핸들을 가지고 대부분 예제가 작성되어 있음.
다만 프로세스 리스트를 출력해야한다면 ID를 활용해야 할 때가 있음.
12.1-2 생성 가능한 쓰레드 개수는?
Note) 관련 예제: Ex 12-1
OS 디자인에 따라 생성 가능한 쓰레드의 개수는 지정 가능함.
보통은 허용 가능한 최대치로 지정 되어 있음. 그럼 이게 몇개일까?
프로세스가 생성되면 일단 4GB 중 보통 2GB가 유저 영역임.
그리고 이 2GB 중, Code/Data/Heap/Stack 영역이 나뉨.
쓰레드는 이중에서 Stack 영역만 독립적으로 갖게됨.
즉, 생성하는 쓰레드 별로 2GB 중 일부분을 Stack 영역 할당 해 주다가,
더이상 불가능할때까지 생성 해준다고 보면 됨. 이걸 알아보는 예.
Ex12-1)
<hide/>
/*
CountThread.cpp
프로그램 설명: 생성 가능한 쓰레드의 개수 측정.
*/
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#define MAX_THREADS (1024*10)
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
DWORD threadNum = (DWORD) lpParam;
while(1)
{
_tprintf(_T("thread num: %d \n"), threadNum);
Sleep(5000);
}
return 0;
}
DWORD cntOfThread = 0;
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadId[MAX_THREADS];
HANDLE hThread[MAX_THREADS];
// 생성 가능한 최대 개수의 쓰레드 생성
while(1)
{
hThread[cntOfThread] =
CreateThread (
NULL, // 디폴트 보안 관리자.
0, // 디폴트 스택 사이즈. -> 이 값을 변경하면서, 최대 생성 개수를 확인 할 수도있음.
ThreadProc, // 쓰레드 main 함수(쓰레드 함수) 설정.
(LPVOID)cntOfThread, // 쓰레드 함수의 전달인자.
0, // 디폴트 쓰레드 생성 속성.
&dwThreadId[cntOfThread] // 쓰레드 ID 저장을 위한 주소값 전달.
);
// 쓰레드 생성 확인
if (hThread[cntOfThread] == NULL)
{
_tprintf(_T("MAXIMUM THREAD SIZE: %d \n"), cntOfThread);
break;
}
cntOfThread++;
}
for(DWORD i=0; i<cntOfThread; i++)
{
CloseHandle(hThread[i]);
}
return 0;
}
12.1-3 쓰레드의 소멸 Case 1
Note) 관련 예제: Ex 12-3
쓰레드 종료시 return을 이용하면 좋은 경우
거의 모든 경우에 해당함. 왜냐하면,
1. 쓰레드를 생성할 때, 먼저 존재이유를 결정
존재 해야만한다면 쓰레드의 작업 영역을 결정지음.
2. 쓰레드는 결국 전역 함수이기 때문에, 언제 어디서건 호출 가능함.
그냥 그 전역 함수의 작업 영역 즉 기능만 쓸려고 호출한거지
콜러의 라이프사이클까지 콜리에게 일임하기 위해서 호출한게 아님.
3. 즉, 1번 2번에 의해서 책임자가 사라져버림.
왜냐면 콜리도 또 다른 쓰레드를 만들어서 전역 함수가 호출되면
그 전역 함수에게 라이프 사이클을 맡길 수 있기 때문.
이런 이유에서 쓰레드의 소멸 Case 2와 Case 3은 아에 배제해도 될정도.
// If the below function fails, the return value is zero.
BOOL GetExitCodeThread(HANDLE hThread, LPWORD lpExitCode);
// 프로세스가 종료될때와 마찬가지로, 쓰레드도 종료될때 반환값이 커널 오브젝트 속에 저장됨.
// 이 값을 GetExitCodeThread() 함수를 통해서 얻을 수 있음.
// 이를 통해서 데이터 통신을 하고 있음.
Ex12-3)
<hide/>
/*
ThreadAdderOne.cpp
프로그램 설명: 프로세스 유사형태 쓰레드 생성.
*/
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
// 쓰레드 메인 함수는 아래와 같이 간단명료한게 최고임.
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
DWORD * nPtr = (DWORD *) lpParam;
// 아래는 전달된 인자를 파싱하는 과정.
DWORD numOne = *nPtr;
DWORD numTwo = *(nPtr+1);
DWORD total = 0;
for(DWORD i=numOne; i<=numTwo; i++)
{
total += i;
}
return total;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[3];
HANDLE hThread[3];
DWORD paramThread[] = {1, 3, 4, 7, 8, 10};
DWORD total = 0;
DWORD result = 0;
hThread[0] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[0]),
0, &dwThreadID[0]
);
hThread[1] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[2]),
0, &dwThreadID[1]
);
hThread[2] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[4]),
0, &dwThreadID[2]
);
if(hThread[0] == NULL || hThread[1] == NULL || hThread[2] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
// 메인 쓰레드가 종료되면 프로세스가 종료되버림.
// 그래서 쓰레드들이 종료될 때까지 메인 쓰레드가 대기하고 있는것.
WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
// 이 함수 뒤로는 세 쓰레드가 모두 종료되었고, 반환 코드가 커널 오브젝트에 적혔단 뜻.
GetExitCodeThread(hThread[0] , &result);
total += result;
GetExitCodeThread(hThread[1] , &result);
total += result;
GetExitCodeThread(hThread[2] , &result);
total += result;
_tprintf(_T("total (1 ~ 10): %d \n"), total);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
return 0;
}
12.1-4 쓰레드의 소멸 Case 2
Note) 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우
특정 위치에서 쓰레드의 실행을 종료시키고자 하는 경우. -> 쓰지말자
12.1-5 쓰레드의 소멸 Case 3
Note) 쓰레드 종료 시 TerminateThread() 함수 호출이 유용한 경우
외부에서 쓰레드의 실행을 종료시키고자 하는 경우. -> 쓰지말자.
12.2 쓰레드의 성격과 특성
12.2-1 Code/Data/Heap 영역의 공유 검증
Note) 관련 예제: Ex12-4
Ex12-4)
Ex12-3에서는 계산된 결과를 반환하는 식으로 함.
4번에서는 Data 영역에 선언된 전역 변수 total에 저장하는 형식.
추후에는 Heap 영역에 동적할당해서 공유 검증을 해봐도 좋을듯.
<hide/>
/*
ThreadAdderTwo.cpp
프로그램 설명: 전역변수를 이용한 쓰레드 기반 Adder.
*/
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
static int total = 0;
DWORD WINAPI ThreadProc( LPVOID lpParam ) // 모든 쓰레드가 ThreadProc() 전역함수를 공유함 즉 Code 영역이 공유되고 있단 뜻.
{
DWORD * nPtr = (DWORD *) lpParam;
DWORD numOne = *nPtr;
DWORD numTwo = *(nPtr+1);
for(DWORD i=numOne; i<=numTwo; i++)
{
total += i;
}
return 0; // 정상적 종료.
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[3];
HANDLE hThread[3];
DWORD paramThread[] = {1, 3, 4, 7, 8, 10};
hThread[0] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[0]),
0, &dwThreadID[0]
);
hThread[1] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[2]),
0, &dwThreadID[1]
);
hThread[2] =
CreateThread (
NULL, 0,
ThreadProc,
(LPVOID)(¶mThread[4]),
0, &dwThreadID[2]
);
if(hThread[0] == NULL || hThread[1] == NULL || hThread[2] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
_tprintf(_T("total (1 ~ 10): %d \n"), total);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
return 0;
}
12.2-2 동시 접근의 문제점
Note) race condition 도식화
사실 위 예제는 정상적인 결과를 얻기 힘든 예제
왜냐면 Data 영역에 선언된 total 변수에 둘 이상의 쓰레드가 동시 접근하기 때문. 이건 문제가 될 수 있음.
근데 혹자는 "쓰레드가 동시 접근한다기 보다는 스케줄러에 의해서 시간을 나눠서 접근하기 때문에
동시 접근은 말이 안된다." 할 수 있음. 맞는 말임. "동시에" 접근하는 것은 일어 날 수 없긴함.
그런데도 다른 곳에서 문제가 생기게 됨.
1. 10이라는 값이 total에 저장되어 있는 상태.
A 쓰레드는 total에 6을 더하려하고 있고, B 쓰레드는 total에 9를 더하려 한다고 하자.
둘 다 CS가 발생하며 running 상태가 된다면?
2. 먼저 A 쓰레드에서 작업하기 위해서 total 값을 레지스터를 통해 ALU로 들어감.
그리고 6이 더해져서 다시 레지스터에 16이 저장됨.
그리고 갱신을 하려던 찰나에 쓰레드 간의 CS가 발생.
실행의 대상이 바뀌니까 레지스터 정보들은 저장되어야 함.
그래서 16을 메모리에 잠시 저장 해둠. 다음 쓰레드를 위해 레지스터는 정리됨.
3. 이제 쓰레드 B는 똑같이 total을 레지스터에 가져와서 9를 더하려 함.
9를 더해서 운좋게도 타임 슬라이스안에 total값을 19로 갱신 해냄.
할일을 마친 쓰레드 B는 다시 A로 실행 우선권이 넘겨짐
4. 다시 돌아온 A는 못다한 갱신 작업이 남음. 메모리 상에 저장되어 있던 자신의 레지스터 정보
16을 읽어 들여서 total값을 16으로 변경시킴.
즉, 위 단계를 보면 진짜 동시에 접근한다는게 아니라 레지스터가 그 문제의 원인.
레지스터의 데이터를 저장하고 복원하는 과정에서, 빈번한 CS과 동일한 메모리 접근이 있다면
문제가 될 수 있음. 이를 가리켜 메모리의 동시접근 문제라 함.
이 문제를 막기 위해서는 A 쓰레드가 total에 접근하는 동안에는 B 쓰레드가 total에 접근하면 안됨.
반대로 B 쓰레드가 total에 접근하는 동안에는 A 쓰레드가 접근하면 안됨.
다시 말하면 빈번한 CS이 문제가 아니라, 둘 중 하나가 접근을 끝내면 다른 하나가 들어가야 한단 것.
total이라는 변수 문제가 아님. total에 접근하는 코드가 문제가 됨. 이런 코드 블럭을 임계 영역이라함.
둘 이상의 쓰레드가 동시에 실행하면 안되는 코드 블럭을 임계 영역이라 한다.
12.2-3 프로세스로부터의 쓰레드 분리
Note)
1. 쓰레드 Usage Count: 생성과 동시에 2. 자신의 생성때문에 1, 생성을 요청한 쓰레드에게 핸들반환 2
2. 하나는 쓰레드 종료 시 감소, 다른 하나는 CloseHandle() 함수 호출 시 감소.
즉, 쓰레드가 종료되었다고 해서 커널 오브젝트가 사라지진 않음.
만약 쓰레드가 종료되면 커널 오브젝트도 사라져서 메모리 낭비를 막아야 한다면
생성과 동시에 CloseHandle() 함수 호출.
3. 쓰레드 생성과 동시에 CloseHandle() 함수 호출: 쓰레드 분리.
단, 주의점은 쓰레드의 반환 코드를 얻고자 한다면 CloseHandle() 전에 해야 함.
12.2-4 ANSI 표준 C 라이브러리와 쓰레드
Ex) CreateThread() and ExitThread() -> _beginthreadex, _endthreadex
<hide/>
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
TCHAR string[] = _T("Hey, get a life!") _T("You don't even ... ...together.");
TCHAR seps[] = _T(",.!");
// 토근 분리 조건, 문자열 설정 및 첫 번째 토근 반환.
TCHAR* token = _tcstok(string, seps);
while (NULL != token)
{
_tprintf(_T(" %s\n"), token);
token = _tcstok(NULL, seps);
}
/*
- strtok() 함수는 결국 어딘가에 정적 변수로건 전역 변수로건 문자열을 저장해두어야 함.
그래야만 반환가능 하기 때문.
- 그렇단 소리는 결국 strtok() 함수를 쓰레드 메인 함수로 하는 쓰레드들이 있을때
임계 영역이 발생할 수 있단 소리.
- 그래서 예전의 ANSI 표준 C 라이브러리는 쓰레드 개념에서 취약한 부분들이 있어서,
멀티 쓰레드에 안전한 라이브러리의 사용이 필요함.
- 최신 버전 VS를 사용한다면 자동으로 링크가 걸려 있긴 함.
이때부터 CreateThread() and ExitThread() -> _beginthreadex, _endthreadex로 바꿔 써야 함.
- 다만 ExitThread()은 안쓸거라, _endthreadex도 사용할 일이 없음.
- 또 단순하게 생각해보면, CreateThread() 함수가 안좋을거 같지만,
_beginthreadex()도 내부적으로는 CreateThread() 함수를 호출함.
그와동시에 _beginthreadex()로 생성된 쓰레드들은 각자의 메모리 공간을 갖게끔 함.
그래서 같은 메모리에 동시 접근 안해도 되게끔 함.
- 마찬가지로 _endthreadex()는 ExitThread()와 하는 일은 같음
그러나 _beginthreadex()에서 만들어진 각자의 메모리 공간을 해제 작업도 함.
그래도 return을 통해서 하면 자동으로 메모리 공간 해제도 이뤄지기에 return이 최고임.
*/
return 0;
}
12.3 쓰레드의 상태 컨트롤 및 쓰레드의 우선순위 컨트롤
12.3-1 쓰레드의 상태 컨트롤
Note) 윈도우즈에서는 프로세스가 아니라 쓰레드가 상태를 지님.
또한, SuspendThread() 함수를 통해서 running 상태의 쓰레드를
Blocked 상태로 바꿀 수 있음. 주의할 점은, Busy waiting 상태가 아님.
또 Blocked 상태의 쓰레드를 다시 Ready 상태로 돌이키는 함수가 ResumeThread() 함수.
단, Ready에서 Running이나 Running에서 Ready는 철저히 스케줄러의 영역임.
12.3-2 쓰레드의 우선순위 결정 요소
Note) 윈도우즈에서는 프로세스가 아니라 쓰레드가 우선순위를 가짐.
프로세스의 단위에서 우선순위 클래스를 가짐.
예를 들어서, 프로세스 A가 기준 우선순위 9를 가진다면 그 안에서 생성되는 쓰레드들은 모두 9
프로세스 B는 HIGH_PRIORITY_CLASS를 받아서, 13이라면 그 안의 쓰레드들은 모두 13
그래서 해당 프로세스 안의 쓰레드를 한데 묶어서 우선순위를 주기에 "Class"라고 명명한 것.
프로세스의 우선순위가 아님에 주의.
Note) 아래 표는 쓰레드가 생성된 후 지정하는 쓰레드의 우선순위.
노말로 두면 자신의 우선순위 클래스 기본값에 따름.
12.3-3 쓰레드의 우선순위 컨트롤 함수
Note) SetThreadPriority() and GetThreadPriority()를 통해서 쓰레드의 우선순위를 접근할 수 있음.
// If the below function fails, the return value is zero.
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
// If the below function fails, the return value is THREAD_PRIORITY_ERROR_RETURN.
BOOL GetThreadPriority(HANDLE hThread);
'C > [서적] 뇌를 자극하는 윈도우즈 시프' 카테고리의 다른 글
Chapter 14. 쓰레드 동기화 기법2 - 실행 순서 동기화 (0) | 2022.02.10 |
---|---|
Chapter 13. 쓰레드 동기화 기법1 - 임계 영역 접근 동기화 (0) | 2022.02.09 |
Chapter 11. 쓰레드의 이해 (0) | 2022.02.06 |
Chapter 10. 컴퓨터 구조에 대한 세 번째 이야기 (0) | 2022.02.05 |
Chapter 09. 스케줄링 알고리듬과 우선순위 (0) | 2022.02.05 |
댓글