2. 개요
• 스레드란?
• 스레드를 생성해야 하는 경우
• 스레드를 생성하지 말아야 하는 경우
• 처음으로 작성하는 스레드 함수
• Create Thread 함수
• 스레드의 종류
• 스레드의 내부
• C/C++ 런타임 라이브러리에 대한 고찰
• 자신의 구분자 얻기
3. 스레드란?
프로세스
코드
전역 변수들
프로세스 힙
프로세스 자원들
열린 파일들
프로세스
힙
….
환경 블록
스레드 1 스레드 2
TLS TLS
스택 스택
4. 스레드란?
• 프로세스와 스레드는 매우 유사한 모양으로 구성
– 프로세스는 프로세스 커널 오브젝트와 주소공간으로 구성
– 스레드는 스레드 커널 오브젝트와 스레드 스택( 16장 )으로 구성 되어 있
다.
• 프로세스는 스스로 수행 될 수 없다( 4장 )
– 껍데기일 뿐, 단순히 생각한다면 스레드의 저장소로 볼 수도 있다.
– 스레드는 항상 프로세스의 컨텍스트 내에 생성되며, 프로세스 안에서
살아 있을 수 있다.
– 프로세스의 주소 공간 내에 있는 코드를 수행하고 데이터를 다룬다.
5. 스레드란?
• 주소 공간의 공유
– 스레드들은 동일한 코드를 수행할 수도 있고, 동일 데이터를 조작할 수도 있다.
– 커널 오브젝트 핸들 테이블은 스레드 별로 존재하는 것이 아니라 프로세스별로 존재하
기 때문에 커널 오브젝트 핸들도 역시 공유.
• 프로세스에 비해 가벼움
– 프로세스는 자신만의 주소 공간을 가지기 때문에 스레드에 비해 더욱 많은 시스템
리소스를 사옹
– exe, dll파일이 주소 공간으로 로드되어야 하므로 파일 리소스 또한 필요
– 반면 스레드는 하나의 커널 오브젝트와 스레드 스택 정도만을 필요로 할 뿐
– 컨텍스트의 양이 적기 때문에 상대적으로 프로세스에 비해 전환이 빠르다.
6. 스레드를 생성해야 하는 경우
• CPU를 놀게 하기 싫을 때!!
– 모든 프로세스는 적어도 하나 이상의 스레드를
가질 수 있고, 아주 특별한 작업을 수행하는 경우가 아닌 이상 운
영체제가 다수의 스레드를 동시에 수행 할 수 있다는 것은 매우 많
은 이점을 제공.
– Visual Studio IDE, Vista의 인덱싱, 스프레드 시트의 백그라운드
재계산 작업, 워드 프로세스의 철자 계산 등등
– I/O 성능에 의해 결정되는 프로그램들의 성능을 향상 시킬 때
(순차적 파일들을 처리 할 때에는 읽기 및 쓰기 연산을 한꺼번에
최대한 많이 수행하는 것이 더 효율적)
7. 스레드를 생성하지 말아야 하는 경
우
• 위험성과 문제점
– 한 프로세스의 스레드들은 저장소와 자원들을 공유하므로,
한 프로세스가 다른 스레드의 자료를 잘못 수정할 수 있다
(경쟁조건이나 교착 같은 결함이 발생 할 수 있다)
– 특정한 상황에서는 동시성이 성능을 향상시키는 게 아니라
크게 떨어뜨리기도 한다.
– 기존의 단일 스레드 프로그램을 스레드들을 활용하도록 변
환하는 것이 어려울 수 있다. 위의 사항들이 이유일 수도 있
고, 프로그램 이해 부족이 이유 일 수도 있다.(ㅜ_ㅜ)
8. 처음으로 작성하는 스레드 함수
• 스레드의 진입점 함수 원형
– 스레드 함수는 우리가 원하는 작업은 어떤 것이라도 수행할 수 있다.
– 스레드 함수는 언젠가는 끝날 것이고 반환될 것이다.
– 스레드 함수가 WINAPI ThreadFunc(VOID 수행을 멈추고
DWORD 반환되는 시점에 스레드는 pvParam)
스레드가 사용하던 스택도 반환된다.
{
DWORD dwResult = 0;
– 스레드 커널 오브젝트의 사용 카운트도 감소한다.
…
– 이 값이 0이 되면 스레드 커널 오브젝트는 파괴된다.
Return(dwResult);
}
– 프로세스 커널 오브젝트와 마찬가지로 스레드 커널 오브젝트 또한
이를 통해 관리되는 스레드 만큼은 살아 있으며 스레드 종료 이후에도 여
전히 살아 있을 수 있다.
9. 처음으로 작성하는 스레드 함수
• 모든 스레드는 수행을 시작할 진입점 함수(entry point)를 반드시
가져야 한다.
– 스레드 함수는 우리가 원하는 작업은 어떤 것이라도 수행할 수 있다.
– 스레드 함수는 언젠가는 끝날 것이고 반환될 것이다.
– 스레드 함수가 반환되는 시점에 스레드는 수행을 멈추고
스레드가 사용하던 스택도 반환된다.
– 스레드 커널 오브젝트의 사용 카운트도 감소한다.
– 이 값이 0이 되면 스레드 커널 오브젝트는 파괴된다.
– 프로세스 커널 오브젝트와 마찬가지로 스레드 커널 오브젝트 또한
이를 통해 관리되는 스레드 만큼은 살아 있으며 스레드 종료 이후에도 여
전히 살아 있을 수 있다.
10. 처음으로 작성하는 스레드 함수
• 스레드 함수는 반드시 값을 반환해야 한다.
– 이 값은 나중에 스레드의 종료 코드가 되며, C/C++ 런타임 라이브
러리가 주 스레드의 종료 코드를 프로세스의 종료 코드로 사용하
는 것과 유사하다.
• 스레드 함수는 가능한 함수로 전달된 매개변수와 지역 변수만을
사용하도록 작성되는 것이 좋다.
– 정적(static)변수나 전역(global)변수를 사용하게 되면 다수의 스레드가 동시에 변
수에 접근할 수 있게 되며, 이는 변수의 값이 잘못 변경되는 원인이 되기도 한다.
– 하지만 함수의 매개변수와 지역변수는 스레드의 스택에 유지되기 때문에 다른 스
레드에 의해 내용이 변경될 가능성이 거의 없다.
13. CreateThread 함수
• LPSECURITY_ATTRIBUTES
– 스레드 커널 오브젝트에 대해 기본 보안 특성을 사용할 것이라면 이 매개변수로 NULL
을 전달하면 된다.
– 나머지는 취향것 MSDN 참조
• StackSize
– 스레드가 자신의 스택을 위해 얼마만큼의 주소 공간을 사용할지를 지정할 수 있다.
– StackSize 0 이외의 값을 지정하면 함수는 스레드 스택을 확보하기 위해 지정된
크기의 메모리를 예약하고 커밋까지 수행한다.
– 만일 시스템이 스택의 최대 크기를 제한하지 않는다면 이러한 재귀호출은 절대로
종료되지 않을 것이다.
– /STACK:[reserve][,commit]
• reserve 인자는 시스템이 스레드스택을 위해 지정된 크기 만큼의 주소 공간을 예약(기본 1M)
• Commit 인자는 스택으로 예약된 주소 공간에 커밋된 물리적 저장소의 초기크기를 나타낸다.
14. 스레드의 종료
• 스레드의 종료
– 스레드 함수가 반환된다( Excellent!! )
– 스레드 함수 내에서 ExitThread 함수를 호출한다
– 동일한 프로세스나 다른 프로세스에서
TerminateThread함수를 호출한다.
– 스레드가 포함된 프로세스가 종료된다.
15. 스레드의 종료
• 스레드 함수가 반환된다.
– 항상 스레드 함수가 반환되도록 설계하는 것이
좋다. 이것은 스레드가 사용한 자원을 적절하
게 정리 할 수 있는 유일한 방법이다.
16. 스레드의 종료
• 스레드 함수가 반환된다.
스레드 함수 내에서 생성한 시스템은 스레드의 종료 코드를
운영체제는 스레드 스택으
모든 C++ 오브젝트들은 스레드 함수의 반환 값으로 설정
로 사용했던 메모리 반환
파괴자를 통해 적절히 제거 (스레드 커널 오브젝트에 저장)
시스템은 스레드 커널 오브
젝트의 사용 카운터를 감소
17. 스레드의 종료
• ExitThread 함수
– 스레드를 강제 종료(반환되지 않는 함수).
스레드를 강제 종료, 시스템은 스레드의 종료 코드를
운영체제가 스레드에 운영체제는 스레드 스택으
스레드 함수의 반환 값으로 설정
사용했던 모든 운영체제 로 사용했던 메모리 반환
(스레드 커널 오브젝트에 저장)
리소스를 종료
스레드 함수 내에서 생성한
모든 C++ 오브젝트들은
파괴자를 통해 적절히 제거 시스템은 스레드 커널 오브
젝트의 사용 카운터를 감소
18. 스레드의 종료
• TerminatedThread 함수
– 어떠한 스레드라도 종료 가능(비동기 함수)
스레드를 강제 종료,
운영체제가 스레드에 시스템은 스레드의 종료 코드를
시스템은 스레드 커널 오브
사용했던 모든 운영체제 스레드 함수의 반환 값으로 설정
젝트의 사용 카운터를 감소
리소스를 종료 (스레드 커널 오브젝트에 저장)
스레드 함수 내에서 생성한
운영체제는 스레드 스택으
모든 C++ 오브젝트들은
로 사용했던 메모리 반환
파괴자를 통해 적절히 제거
19. 스레드의 종료
• 프로세스 종료시
– 프로세스가 소유한 모든 스레드 종료
• 프로세스가 사용중 이던 리소스들도 모두 정리
• 프로세스 내에 남아 있던 스레드들에 대해 각각
TerminatedThread 함수가 호출!!!!
20. 스레드의 종료
• 스레드가 종료되면
– 모든 유저 오브젝트 핸들이 삭제
• 윈도우와 윈도우 훅 두 개의 사용자 오브젝트는 스레드에 의해 소유, 스레드가 종료되면 시스템
은 자동적으로 해당 스레드가 생성한 윈도우를 파괴하고, 설치된 윈도우 훅을 제거한다.
• 다른 형태의 오브젝트들은 모두 소유하고 있는 프로세스가 종료되는 시점에 파괴된다.
– 스레드의 종료 코드는 STILL_ACTIVATE에서 exitThread나 TerminatedThread에서 지
정한 종료코드로 변경
– 스레드 커널 오브젝트의 상태가 시그널 상태로 변경
– 종료되는 스레드가 프로세스 내의 마지막 활성 스레드라면 시스템은 프로세스도 같이
종료되어야 하는 것으로 간주
– 스레드 커널 오브젝트의 사용 카운트가 1만큼 감소
스레드의 종료 여부를 확인하기 위해 어떻게 스레드 핸들을 사용할 지에
대해서는 9장에서~
21. 스레드의 내부
스레드 커널 오브젝트
스레드 스택
컨텍스트
SP pvParam
IP pfnStartAddr
다른 CPU 레지스터 .
.
.
다른속성과 통계정보
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1
VOID RtlUserThreadStart(…){}
종료 코드 = STILL_ACTIVATE
시그널 상태 = FALSE
22. 스레드의 내부
스레드 커널 오브젝트
사용카운트
CreateThread 함수가 반환한 핸들이
스레드 스택
제거될 때까지 파괴되지 않는다.)
컨텍스트
SP pvParam
IP pfnStartAddr
다른 CPU 레지스터 .
.
.
다른속성과 통계정보
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1
VOID RtlUserThreadStart(…){}
종료 코드 = STILL_ACTIVATE
시그널 상태 = FALSE
23. 스레드의 내부
스레드 커널 오브젝트
사용카운트
CreateThread 함수가 반환한 핸들이
스레드 스택
제거될 때까지 파괴되지 않는다.)
컨텍스트
SP pvParam
IP pfnStartAddr
스레드 커널 오브젝트의 초기화
다른 CPU 레지스터 .
.
.
다른속성과 통계정보
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1
VOID RtlUserThreadStart(…){}
종료 코드 = STILL_ACTIVATE
시그널 상태 = FALSE
24. 스레드의 내부
스레드 커널 오브젝트
사용카운트
CreateThread 함수가 반환한 핸들이
스레드 스택
제거될 때까지 파괴되지 않는다.)
컨텍스트
SP pvParam
IP pfnStartAddr
스레드 커널 오브젝트의 초기화
다른 CPU 레지스터 .
.
.
스레드 스택으로 활용할
메모리 공간 할당
다른속성과 통계정보 (프로세스 주소 내 공간)
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1
VOID RtlUserThreadStart(…){}
종료 코드 = STILL_ACTIVATE
시그널 상태 = FALSE
25. 스레드의 내부
스레드 커널 오브젝트
사용카운트
CreateThread 함수가 반환한 핸들이
스레드 스택
제거될 때까지 파괴되지 않는다.)
컨텍스트
SP pvParam
IP pfnStartAddr
스레드 커널 오브젝트의 초기화
다른 CPU 레지스터 .
.
.
스레드 스택으로 활용할
메모리 공간 할당
다른속성과 통계정보 (프로세스 주소 내 공간)
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1 새로 생성된 스레드 스택의 가장
VOID RtlUserThreadStart(…){} 상위에 두 개의 값을 기록
종료 코드 = STILL_ACTIVATE 1. pvParam
2. pfnStartAddr
시그널 상태 = FALSE
26. 스레드의 내부
스레드 커널 오브젝트
스레드 컨텍스트
스레드가 마지막으로 수행되었을 때
스레드 스택
당시의 CPU 레지스터 값을 가지고 있다.
컨텍스트
SP pvParam
IP pfnStartAddr 인스트럭션 포인터(IP)와 스택 포인터(SP)
프로세스 컨텍스트 내부에서 수행되기 때문에,
프로세스 메모리 공간 상의 특정 위치를
다른 CPU 레지스터 .
. 가리키고 있다.
.
스레드 커널 오브젝트가 초기화되면 Context구조
체 내의 스택 포인터 레지스터는 pfnStartAddr를
다른속성과 통계정보 저장하고 있는 스레드 스택의 주소로 설정
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1 IP는 NTDLL.dll 모듈이 익스포트 하고
VOID RtlUserThreadStart(…){} 있는 RtlUserThreadStart라는
종료 코드 = STILL_ACTIVATE 문서화 되지 않은 함수의 주소를
가리키도록 설정
시그널 상태 = FALSE
27. 스레드의 내부
스레드 커널 오브젝트
스레드의 초기화가 완료되면
CREATE_SUSPENDED플래그가 전달되
스레드 스택 었는지 확인, 전달되지 않았다면 정지 카
컨텍스트 운트를 0으로 설정 스레드가 프로세서에
pvParam
스케줄될 수 있도록 한다.
SP
IP pfnStartAddr 스레드가 CPU 시간을 얻으면 시스템은 스레드
컨텍스트에 마지막으로 저장된 값을 CPU 레지
다른 CPU 레지스터 . 스터로 로드, 프로세스 주소 공간 내에 있는
. 코드를 수정하고 데이터를 변경하는 등의
. 작업을 수행한다.
새로운 스레드의 IP가 RtlUserThreadStart로 설정,
다른속성과 통계정보 실질적으로 수행하는 최초 위치가 된다.
사용 카운트 = 2
NTDLL.dll
정지카운트 = 1
VOID RtlUserThreadStart RtlUserThreadStart 함수는 C/C++ 런타임 라이브
종료 코드 = STILL_ACTIVATE ( pfnStartAddr, pvParam) 러리의 시작 코드를 호출하여 각종 초기화를 진행
하고, _tmain이나 _tWinMain과 같은 진입점 함수
를 호출한다.
시그널 상태 = FALSE
진입점 함수가 반횐되면 C/C++ 애플리케이션의
주 스레드는 RtlUserThreadStart 함수로 절대 반
환되지 않는다.
28. C/C++런타임 라이브러리에 대한 고찰
• Visual Studio의 C/C++런타임 라이브러리
– 4가지의 네이티브 런타임 라이브러리
– 2가지 형태의 매니지드 런타임을 포함
– 모든 라이브러리들이 멀티스레드 개발을 지원하므로 더 이상
싱글스레드 전용의 c/c++ 라이브러리는 제공되지 않는다.
• 표준 C 런타임 라이브러리
– 운영체제에 스레드라는 개념이 도입되기 한참 전인 1970년대에 처음 개발
– 멀티스레드 애플리케이션에서 C 런타임 라이브러리를 사용했을 때 발생하는 문제에 대해서 전혀 고려하지 않음
– 수행중인 스레드가 system함수를 호출하고 if 문장이 수행되기 직전에 인터럽트 될 시, 동일 프로세스 내에 수행 중인
두 번째 스레드가 수행, 이 스레드가 다른 C 런타임 함수를 호출하여 errorno의 전역 변수 값을 바꾸어 버릴 수 있다.
• Errno, _doserror, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt, _fcvt등등
– 멀티스레드 기반의 C/C++ 프로그램이 정상적으로 동작하려면 C/C++ 런타임 라이브러리 함수들을 사용하는
각 스레드별로 적절한 구조의 데이터 블록을 생성해야 한다.
29. C/C++런타임 라이브러리에 대한 고찰
• _beginthreadex
– 운영체제는 새로운 스레드가 생성되었을 때 어떻게 새로운 데이터 블록을 할당해야 할 지 알 수없다.
– 개발자는 이 모든 것이 정상적으로 수행될 수 있도록 해 주어야 할 막중한 책임이 있다!!.
– C/C++런타임 라이브러리가 제공하는 _beginthreadex를 호출해 주면 된다.
• _begintreadex 함수의 특징
– 각 스레드는 c/c++ 런타임 라이브러리 힙에 _tiddata메모리 블록을 가진다.
– _beginthredex 함수에 전달된 스레드 함수의 주소는 _tiddata메모리 블록 내에 저장된다.(Mtdll.h 에
서 확인 가능)
– 내부적으로 CreateThread함수를 호출, pfnStartAddr 매개변수로 전달한 스레드 함수가 아니라
_threadstartex 라는 함수가 수행하게 된다. 또한 스레드 함수로 전달할 매개변수도 pvParam이 아니
라 _tiddata 구조체의 주소다.
– 정상적인 경우 CreateThread와 동일하게 스레드 핸들을 반환하고, 문제가 있으면 0을 반환한다.
30. C/C++런타임 라이브러리에 대한 고찰
• _threadstartex
– 새로 생성된 스레드는 RtlUserThreadStart(NTDLL.dll)를 호출하고 곧 _threadstartex로 진입
– 새로 생성된 스레드의 _tiddata 블록을 가리키는 주소가 매개변수로 전달
– TlsSetValue는 이 함수를 호츨하는 스레드와 매개변수로 전달되는 값을 연계시키는 운영체제 함수다.
이러한 값이 저장 되는 공간을 스레드 지역 저장소 (TLS)라고 한다.
– 사용자 정의 스레드 함수가 전달한 매개변수 값으로 호출된다. 스레드 함수의 주소와 매개 변수 값은
_begintreadex 함수 내에서 TLS에 저장하였던 _tiddata 블록을 _callthreadstartex 함수 내에서 가져
와서 사용한다.
– 사용자가 지정한 스레드 함수의 반환 값은 스레드의 종료 코드가 된다.
– Callthreadstaartex는 단순히 _threadstartex로 반환되고, 계속해서 RtlUserThreadStart로 반환되는
구조가 아님에 주목, 만일 그렇다면 스레드는 종료되고, 스레드의 종료 코드는 올바르게 설정될지 모
르겠지만 _tiddata 메모리 블록은 해제되지 않을 것며 이것은 메모리 누수를 일으키지 않기 위해
_endtreadex라는 c/c++ 런타임 라이브러리 함수가 제공되며, 이 함수는 매개변수로 스레드 종료 코
드로 전달 받는다.
31. C/C++런타임 라이브러리에 대한 고찰
• _endtreadex
– c/c++런타임 라이브러리 함수인 _getpid_noexit함수는 이 함수를 호출하는 스레드의 _tiddata 메모리 블록을 가져오기
위해 내부적으로 운영체제의 TlsGetValue 함수를 호출
– _tiddata 블록이 삭제되고 운영체제의 ExitThread함수가 호출되어 스레드를 실제로 파괴한다. 물론 이 과정에서 종료 코
드가 전달되고 올바르게 설정된다.
– C/C++런타임 라이브러리가 제공하는 _beginthreadex를 호출해 주면 된다.
• 그외
– c/c++런타임 라이브러리는 두개의 스레드가 힙으로부터 동시에 메모리를 할당하는 것을 금지하고 있다. 호출한 malloc
함수로 부터 첫 번째 스레드가 반환될 때까지 두 번째 스레드는 malloc 함수를 수행하지 못하고 대기 상태가 되며, 첫 번
째 스레드가 반환된 이후라야 비로소 두 번째 스레드는 malloc 함수 내부로 진입 할 수 있다.
– c/c++ 런타임 라이브러리의 dll 버전은 다른 애플리케이션이나 dll과 공유될 수 있도록 작성되어 있다. 따라서
멀티스레드 버전의 dll 라이브러리는 단 한번만 로드되면 된다. c/c++ 런타임 라이브러리가 dll 형태로 제공되기 때문에
애플리케이션과 dll은 c/c++ 런타임 라이브러리 함수의 코드를 각각 가질 필요가 없고, 그 결과로 생성파일은
좀 더 작아지고 ms가 c/c++ 런타임 라이브러리 dll 파일 내의 버그를 수정하게 되면 애플리케이션은 자동적으로
이러한 수정사항을 반영하게 된다.
32. C/C++런타임 라이브러리에 대한 고찰
• 실수로 _beginthreadex 대신 createThread
를 호출하였다면
– 그냥 _beginThreadex 씁시다(힘들어요)
• 절대로 쓰지 말아야 하는 c/c++ 런타임 라
이브러리 함수
– 마찬가지입니다. _beginThreadex, ex ex!!!
33. 자신의 구분자 얻기
• 자신의 구분자 얻기(핸들 얻기)
– 236~ 240page( 저도 하고 싶었어요;;)
– 대부분은 허위 핸들에서 작업 가능하지만 다른
프로세스 쪽에 전달해서 쓰거나 제거하게 되면
Hell…