스레드(Thread)
August 15, 2019
목표
- 스레드를 생성해야 하는 경우
- 스레드를 생성하지 말아야 하는 경우
- CreateThread 함수
- 스레드의 종료
- 스레드의 내부
- C/C++ 런타임 라이브러리에 대한 고찰
- 자신의 구분자 찾기
이번 포스팅에서는 프로세스와 스레드는 어떻게 다른지, 그리고 각각은 서로 어떠한 책임이 있는 지에 대해 알아볼 것이다. 또한 시스템이 스레드를 다루기 위해 사용하는 커널 오브젝트에 대해서도 알아보자.
스레드는 2개의 요소로 구성되어 있다.
- 운영체제가 스레드를 다루기 위한 스레드 커널 오브젝트.
- 스레드가 코드를 수행할 때 함수의 매개변수와 지역변수를 저장하기 위한 스레드 스택
전에 프로세스는 스스로 수행될 수 없다고 배웠다. 프로세스는 그 자체로는 어떤 것도 수행할 수 없으며 단순히 생각하면 스레드의 저장소라고 볼 수 있다. 스레드는 항상 프로세스의 컨텍스트 내에서 생성되며 프로세스 안에만 살아 있을 수 있다. 즉 스레드는 프로세스의 주소 공간 내의 코드를 수행하고 데이터를 다룬다.
하나의 프로세스내에 둘 이상의 스레드가 존재하는 경우
- 스레드들은 단일 주소 공간을 공유한다.
- 이들은 동일한 코드를 수행할 수 있으며 동일한 데이터에 접근할 수도 있다.
- 커널 오브젝트 핸들도 공유한다. (커널 오브젝트 핸들 테이블은 프로세스당 1개!)
프로세스는 자신만의 주소 공간을 가지지만 상당한 시스템 정보를 내부에 저장해야 하기 때문에 메모리를 많이 차지하며 dll이나 exe 파일을 로드하기 위한 리소스 또한 필요하다. 반면에 스레드는 단지 하나의 커널 오브젝트와 스택 정도만을 필요로 한다. 스레드는 프로세스에 비교하여 부하가 적기 때문에 프로세스를 새로 생성하는 대신 스레드를 생성하여 문제를 해결하도록 시도해 보는 편이 좋다.
스레드를 생성해야 하는 경우
스레드는 프로세스 내의 실행 흐름을 의미한다. 프로세스가 초기화 되는 동안에 시스템은 스레드를 생성하고 C/C++ 런타임 라이브러리의 시작 코드를 실행한다. 이후에 진입함수(_tmain, _tWinMain)가 실행될 것이다. 진입함수가 반환되면 런타임 라이브러리의 시작 코드가 ExitProcess를 호출하여 수행을 종료한다. 대부분의 프로그램은 이와 같이 메인 스레드 하나의 흐름으로 충분하다.
하지만 컴퓨터는 CPU라는 매우 강력한 리소스를 가지고 있고 이것을 쉬게 만들 필요가 없으며 대부분의 경우 CPU가 계속해서 작업을 할 수 있도록 해 주는 것이 좋다.
예를 들면
- 윈도우 운영체제의 인덱스 서비스(index service)는 낮은 우선순위로 생성되어 주기적으로 디스크 드라이브에 있는 파일에 대해 인덱싱을 수행한다. 인덱싱을 수행하면 디스크 드라이브에서 파일을 검색할 때 파일을 열고, 검색하고, 닫는 작업을 수행하지 않아도 되기 때문에 상당한 검색 속도 향상을 가져온다.
- 운영체제에 포함된 디스크 조각 모음을 낮은 순위의 스레드를 이용하여 백그라운드에서 조각 모음을 틈틈히 수행할 수도 있다.
- Microsoft visual studio IDE는 사용자가 입력을 멈추었을때 백그라운드에서 소스 코드를 자동적으로 컴파일하고 이를 통해 잘못된 코드에 밑줄을 긋거나 경고 또는 에러를 보여줄 수 있다.
- 워드 프로세서에서도 마찬가지로 철자와 문법 검사, 그리고 프린팅 등의 작업을 수행한다.
- 다른 미디어로의 파일 복사 작업을 백그라운드로 수행한다.
- 웹 프라우저는 백그라운드에서 서버와 통신을 수행한다. 이를 통해 사용자는 접속을 시도한 웹 페이지가 모두 로드되기 전에 다른 사이트로 이동하거나 창의 크기를 변경할 수 있다.
이와 같이 멀티 스레딩을 사용하면 사용자 인터페이스를 단순화시키고, 좀 더 즉각적인 응답을 보이도록 할 수 있으며, 다수의 CPU를 가진 멀티프로세서에 대해서도 더 효율적인 동작을 수행할 수 있다.
CreateThread 함수
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
- CreateThread가 호출되면 시스템은 스레드 커널 오브젝트를 생성한다. 커널 오브젝트가 스레드 자체는 아니며 운영체제가 스레드를 다루기 위한 데이터 구조체에 불과함.
- 프로세스와 프로세스 커널 오브젝트간의 상관관계와 동일
- 시스템은 스레드가 사용할 스텍을 확보한다. 생성된 스레드는 스레드를 생성한 프로세스와 동일한 컨텍스트 내에서 수행됨.
- 따라서 스레드는 프로세스의 모든 커널 오브젝트 핸들과 메모리, 그리고 같은 프로세스에 있는 다른 스레드의 스택에도 접근 가능하다!(의외군)
CreateThread 함수의 매개변수
- lpThreadAttributes: LPSECURITY_ATTRIBUTES를 가리키는 포인터. 기본으로 설정할 것이라면 NULL 전달
- dwStackSize: 스레드가 자신의 스택을 위해 얼마만큼의 주소 공간을 사용할지. 모든 스레드는 자신만의 고유 스택을 가지고 있는데 기본 값은 1MB로 할당됨. 이 값과 링커의 /STACK 스위치중 더 큰 값을 사용하여 스레드 스택을 생성한다. 이 값을 초과하여 스텍을 사용할 시 예외 발생!
- lpStartAddress/lpParameter: StartAddress는 스레드가 호출할 스레드 함수의 주소를 가리킨다. lpParameter는 스레드 함수의 매개변수를 전달하는데 사용됨. void 형이므로 스레드 함수 내부에서 캐스팅하여 사용하며 여러개의 매개변수를 넘겨야 하는 경우에는 배열의 형태로 넘기자.
- dwCreationFlags: 스레드를 생성할 때 세부적인 제어를 수행하기 위한 플래그. (링크)
- lpThreadId: 새로운 스레드에 할당되는 스레드 ID 값을 저장할 변수의 주소를 넘긴다. NULL을 넘기면 스레드 ID에 관심이 없다고 함수에 알려주게 된다.
스레드의 종료
스레드는 4가지 방법으로 종료될 수 있다. 각 방법에 대해 알아보자.
- 스레드 함수가 반환된다(추천)
- 스레드 내에서 ExitThread 함수를 호출(비추)
- 동일한 프로세스나 다른 프로세스에서 TerminateThread 함수를 호출(역시나 비추)
- 스레드가 포함된 프로세스가 종료된다. (이것도 추천은 안함)
1. 스레드 함수의 반환
- 스레드 함수 내에서 생성한 모든 C++ 오브젝트는 소멸자를 통하 적절히 제거된다.
- 운영체제는 스레드 스택으로 사용한 메모리를 반환한다.
- 시스템은 스레드의 종료 코드를 스레드 함수의 반환 값으로 설정함
- 시스템은 스레드 커널 오브젝트의 usage count를 감소시킨다.
2. Exit Thread 함수
- 스레드에서 사용한 운영체제 리소스를 정리하지만 C++ 소멸자 호출 안됨. (메모리 릭)
- 이 함수를 사용하기 보다는 스레드 함수가 반환되도록 코드를 작성하는 것이 좋다.
3. TerminateThread 함수
- ExitThread 함수와는 다르게 이 함수는 어떠한 스레드에서도 사용 가능하다. 즉 스레드가 자신이 아닌 다른 스레드를 종료 가능하다.
- 종료될 스레드는 자신이 종료될 것이라는 사실을 통보받지 못하기 때문에 ExitThread를 사용하는 방법보다 더 나쁘다!
- ExitThread는 스레드가 사용하던 스택 메모리는 반환하지만 TerminateThread는 프로세스가 종료될 때까지 스텍 메모리조차 반환하지 않는다!
4. 프로세스의 종료
- 프로세스가 소유하고 있던 모든 스레드가 종료됨
- 프로세스가 사용하던 리소스는 모두 정리되므로 프로세스 내의 스레드들의 리소스 또한 정리됨
- 프로세스가 종료되는 프로세스 내의 각각의 스레드에 대해 TerminateThread가 호출됨.
5. 스레드가 종료되면 어떤 일이 일어나는가?
- 스레드가 소유하던 모든 유저 오브젝트 핸들이 삭제
- 스레드의 종료 코드는 STILL_ACTIVE에서 ExitThread 또는 TerminateThread에서 지정한 종료 코드로 변경됨
- 스레드 커널 오브젝트의 상태가 시그널 상태로 변환.
- 종료된 스레드가 프로세스의 마지막 스레드라면 프로세스도 종료되어야 하는 것으로 간주됨
- 스레드 커널 오브젝트의 usage count가 1 감소
스레드의 내부
[그림: 스레드 내부]
- CreateThread 함수가 호출되면 시스템은 스레드 커널 오브젝트 생성하며 초기 usage count는 2
- 스레드 커널 오브젝트가 생성되면 시스템은 스레드 스택으로 활용할 메모리 할당(프로세스의 주소 공간으로부터 할당)
- 각 스레드는 자신만의 CPU 레지스터 세트를 가지는데 이를 스레드 컨텍스트라고 부른다.
- instruction pointer(IP), stack pointer(SP) 레지스터는 스레드 컨텍스트에 저장되는 값 중에 가장 중요한 레지스터다. 스레드는 항상 프로세스의 컨텍스트 내부에서 실행되는 사실을 기억하자.
- 위 두 레지스터는 프로세스 메모리 공간 내의 특정 위치를 가리킨다.