쓰레드를 생성할 때는 해당 쓰레드의 작업 영역 범위를 명확히 구분지어야 함.
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
);
Handle과 ID의 차이
헨들은 프로세스 내에서 다른 커널 오브젝트에 접근하기 위해 사용됨.
반면 ID는 범위가 좀 더 넓음. 여러 프로세스를 생성하고 그 안에서 수많은 쓰레드를 만들었을 경우 각 쓰레드의 ID는 고유한 값. ID가 사용되는 경우가 있고, 핸들이 사용되는 경우가 있음. 윈도우즈에서는 보통 핸들을 사용됨.
OS 디자인에 따라 최대 쓰레드 개수를 정할 수도 있지만, 보통 최대치까지 허용.
쓰레드를 생성할 때 새롭게 스택 영역이 할당됨. 일단 32비트 시스템 기준 2GB는 OS가 차지.
나머지 2GB 내에서 프로세스의 스택과는 별도로 쓰레드 별로 스택을 할당하다가. 더 이상 할당할 수 없을 때 까지 쓰레드를 생성할 수 있다.
아래 이미지를 보면 프로세스가 실행될 때 main() 함수를 호출하는 main 쓰레드가 생성됨.
그리고 main쓰레드가 CreateThread()를 통해 새로운 쓰레드 생성 + 별도의 흐름을 갖게 됨. 이렇게 생성된 쓰레드들도 CreateThread() 함수를 통해 새로운 쓰레드를 생성할 수 있다.


프로세스 종료 시 반환 코드 => 커널 오브젝트에 저장
쓰레드 종료 시 반환 코드 => 마찬가지로 쓰레드의 커널 오브젝트에 저장
GetExitCodeThread() 함수를 통해 해당 값을 얻어올 수 있음.
/*
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;
}

/*
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;
}
total이라는 변수에 둘 이상의 쓰레드가 동시에 접근하는 상황.
동시 접근 -> 실행 중에 다른 쓰레드가 접근한다는 것.
동시 접근을 하더라도 사실상 스케줄러에 의해 시간을 나눠서 번갈아 실행되기 때문에 진짜로 동시에 접근하지는 않으나 문제가 생길 수 있음.
1) A 쓰레드는 total + 6, B 쓰레드는 total + 9
2) A 쓰레드 작업 시작. 레지스터->ALU를 거쳐 데이터 A 쓰레드의 연산 결과를 다시 레지스터에 저장하려는 순간 컨텍스트 스위칭이 일어날 경우, 레지스터는 실행 대상의 값으로 채워져야 하기 때문에 기존 값을 메모리에 잠시 저장.
3) B 쓰레드의 작업 시작. 작업이 끝난 후 멈춰놨던 A 쓰레드의 작업을 다시 시작
4) 레지스터에 있는 값을 저장하고 복원하는 위 과정에서 메모리 동시 접근이 문제가 됨.
이를 막기 위해서 A 쓰레드가 메모리에 저장해둔 값에 접근해서 작업하는 동안 B 쓰레드의 접근을 막아야 함. 작업이 끝난 후 접근해야 한다.
임계 영역: 둘 이상의 쓰레드가 동시에 접근하면 안 되는 코드 블럭 (여기서는 메모리에 저장된 total값)

#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;
}
윈도우즈에서는 스케줄러가 프로세스가 아닌 쓰레드를 관리하고, 쓰레드가 상태를 지닌다.
SuspendThread(): Running 상태의 쓰레드를 잠시 Blokced 상태로 멈추는 함수.
ResumeThread(): Ready에서 Runnning으로 바꾸는 함수

쓰레드의 우선 순위 결정 요소
실제로 우선 순위를 갖는 것 또한 프로세스가 아닌 쓰레드.
먼저 프로세스 단위에서 우선 순위 클래스를 가지고, 해당 클래스 안에서 프로세스 우선 순위를 기준으로 쓰레드의 우선 순위를 가진다.
ex) 프로세스의 우선순위가 13인 클래스가 있다면 해당 계층 쓰레드의 우선 순위는 모두 13 + N

