개발/VC++

[스크랩] C++ 스레드 thread

99iberty 2021. 6. 29. 10:11

https://jungwoong.tistory.com/39

 

[window c++] 스레드

프로세스와 스레드의 관계  이전에 프로세스를 설명할 때 프로세스는 최소한 하나의 스레드를 가지고 실질적인 코드의 수행은 스레드에 의해서  실행 된다고 설명 드렸습니다.  프로세스의

jungwoong.tistory.com

프로세스와 스레드의 관계

 이전에 프로세스를 설명할 때 프로세스는 최소한 하나의 스레드를 가지고 실질적인 코드의 수행은 스레드에 의해서

 실행 된다고 설명 드렸습니다. 

 프로세스의 내부의 스레드들은 프로세스의 가상 주소 공간 및 커널 오브젝트 핸들 테이블등 다양한 리소스를

 공유 합니다. 

프로세스와 스레드의 리소스 사용량

싱글 스레드 어플리케이션 메모리 구성

 프로세스는 자신만의 가상주소공간을 가지고 "exe" 또는 "dll" 파일의 주소공간 로드 및 많은 시스템 리소스를 생성

 해야 하기 때문에 스레드 보다 많은 시스템 자원을 사용합니다. 

 스레드는 하나의 커널 오브젝트 및 스레드 스택 정도만 필요하기 때문에 프로세스보다 적은 시스템 리소스를

 사용 합니다. (스레드의 시스템 리소스는 대부분 스레드의 코드 수행을 위한 자원정보 입니다.)

 스레드의 시스템 리소스 사용량이 적기 때문에 프로세스를 새로 생성하는 것 보다는 스레드를 생성해서 처리하는

 것이 시스템 자원상으로 효율적입니다.

멀티 스레드 VS 멀티 프로세스 

멀티 스레드 어플리케이션 메모리구조

멀티 스레드 

 멀티 스레드 구조의 어플리케이션의 장점은 멀티 프로세스에 비해서 적은 시스템 리소스를 사용해서 

 병렬로 코드를 수행할 수 있습니다. 프로세스의 메모리 자원을 공유하기 때문에 프로세스간 통신보다  

 빠르게 자원에 접근할 수 있습니다.

 단점으로는 프로세스 내부에서 하나의 스레드에서 비정상 종료가 발생하면 프로세스 내부에 모든 스레드들이

 종료됩니다. 

 프로세스 내부의 스레드들은 프로세스의 자원을 동시에 접근 할 수 있기 때문에 동기화 작업에 신경써야 합니다.

멀티 프로세스

 멀티 프로세스의 장점은 개별 프로세스의 비정상 종료에 대해서 영향을 받지 않는다는 점과 프로세스가 소유한 자원의

 사용에 대한 동기화에 신경쓰지 않아도 된다는 점입니다. 

 단점으로 프로세스간의 동기화가 필요하다면 프로세스간 통신 작업을 구현해야 하며

 멀티 스레드에 비해서 많은 시스템 자원을 소모한다는 점입니다. 

스레드의 생성

 스레드가 수행되기 위해서는 진입점 함수를 가져야 합니다. 

DWORD WINAPI ThreadFunc(PVOID pvParam)
{
	DWORD dwResult = 0;
	// 스레드 로직 수행
	//....
	Sleep(3000);
	return (dwResult);
}

 프로세스의 주 스레드는 _tmain이나 _tWinMain의 진입점 함수를 가지며 추가적으로 생성되는 스레드는 위와 같은

 형태의 진입점 함수를 가져야 합니다. 스레드는 어떠한 로직도 수행될 수 있으며 진입점 함수가 반환 되면 

 스레드의 수행이 멈추고 스레드의 자원을 반환합니다. 

 스레드는 반환시 결과 값을 리턴하는데 이 값은 스레드이 종료코드로 사용 됩니다. 

 윈도우에서 스레드를 생성하기 위해서는 CreateThread 함수를 사용하면 생성 할 수 있습니다. 

 CreateThread 함수가 호출되면 스레드 커널 오브젝트가 생성되며 스레드가 사용할 스택을 할당 받습니다. 

 생성된 스레드는 스레드를 생성한 프로세스의 컨텍스트 내에서 실행되기 때문에 프로세스의 모든 커널 오브젝트

 핸들과 모든 메모리를 공유해서 사용합니다. 

	// Thread에 전달할 매개 변수입니다.
	DWORD dwArg = 10;
	// Thread ID를 받아 옵니다.
	DWORD dwThreadID;
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (PVOID)&dwArg, 0, &dwThreadID);
	// 스레드가 종료될때까지 대기합니다.
	DWORD dwRet = WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);

 

 위의 코드처럼 CreateThread함수를 호출해서 사용할 수 있습니다.

CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );

 첫번째 인자는 보안특성을 위한 값을 전달합니다. 스레드에 보안 인자를 셋팅하지 않으려면 NULL값을 전달합니다.

 두번째 인자는 스레드의 스택 사이즈를 전달합니다. 0을 전달하면 기본 값인 1MB로 셋팅됩니다.

 세번째 및 네번째 인자는 생성되는 스레드의 진입점 함수 및 전달 파라미터를 지정합니다. 

 다섯번째 인자로는 스레드가 생성될 세부제어를 위한 플레그 값을 전달합니다. 

 마지막 인자는 새로운 스레드의 스레드 ID를 전달 받기 위한 값입니다. 

 스레드의 종료

 스레드도 프로세스와 마찬가지로 다양한 방법으로 종료 될 수 있는데 가장 추천하는 방법은 

 진입점 함수가 반환되서 종료되는 정상적인 종료 방법입니다. 이렇게 스레드가 종료된다면 

  • 스레드 함수 내에서 생성된 C++오브젝트이 소멸자가 정상적으로 호출되며 제거됩니다. 
  • 스레드 스택으로 사용한 메모리를 반환합니다. 
  • 스레드 진입점 함수의 반환값을 스레드 종료 코드로 설정합니다. 
  • OS는 스레드 커널 오브젝트의 사용 카운트를 감소 시킵니다. 
  • 스레드 커널 오브젝트가 시그널 상태로 변경됩니다. 

 ExitThread함수 및 TerminateThread함수로 스레드를 종료 시킬 수 있는데 프로세스의 종료와 마찬가지로 

 해당 함수들로 스레드가 종료되면 C++ 관련 리소스들이 정상적으로 종료 되지 않습니다. 

 TerminateThread로 종료시키면 스레드가 소유한 스택 메모리를 정상적으로 반환하지 못합니다. 

 스레드의 프로세스가 종료하면 스레드는 TeminateThread를 전달 받고 종료됩니다. 

스레드의 내부 구조

스레드 생성시 내부구조

 스레드를 생성하게 되면 위 그림처럼 스레드 커널 오브젝트가 생성됩니다. 

 커널 오브젝트의 사용카운트는 2로 생성됩니다. 

 정지 카운트는 1로 셋팅되는 이 값이 1이상이면 스레드가 수행되지 않고 대기합니다.

 종료코드는 STILL_ACTIVE로 셋팅되며 커널 오브젝트 상태는 논시그널 상태로 설정 됩니다. 

 스레드가 종료되면 스레드 커널 오브젝트 상태는 시그널 상태로 변환하여 스레드의 종료를 알립니다. 

 시스템은 추가로 스레드 스택으로 사용할 메모리를 스레드를 소유한 프로세스의 가상메모리에 할당 합니다. 

 스레드 스택의 가장 상위 값은 CreateThread함수로 전달한 pvParam과 pfnStartAddr로 셋팅됩니다. 

 또한 쓰레드는 자신만의 CPU 레지스터 셋트를 가지는데 스레드 컨텍스트라고 합니다. 

 컨텍스트는 스레드가 수행한 상태의 CPU 레지스터 값을 저장합니다. 이 값은 스레드 커널 오브젝트에 저장됩니다. 

 스레드가 초기화가 완료되면 시스템은 CreateThread 함수 호출시 CREATE_SUSPENDED 플레그가 전달 되었는지

 확인 후에 이 플레그가 없다면 정지 카운트를 0으로 셋팅하여 스레드가 수행될 수 있도록 셋팅합니다. 

 스레드가 CPU 시간을 할당 받으면 스레드 컨텍스트의 CPU 레지스터 정보를 로드하여 수행합니다. 

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
    __try
    {
    	ExitThread((pfnStartAddr)(pvParam));
    }
    __except(UnHandledExceptionFilter(GetExceptionInfomation()))
    {
    	ExitProcess(GetExceptionCode());
    }
}

CreateThread 실행시 호출스택 

 가장 먼저 수행되는 함수는 IP레지스터에 등록된 ntdll.dll의 RtlUserThreadStart함수입니다. 

 RtlUserThreadStart함수에서는 구조적 예외처리(structured exception handling) 프레임이 설정 되며 

 CreateThread 함수로 전달된 매개변수와 진입점 함수를 실행합니다. 

 진입점 함수가 반환되면 ExitThread에 진입점 함수의 리턴값을 종료코드로 셋팅하고 스레드 커널 오브젝트의 사용

 카운트를 감소시키고 스레드의 수행을 종료시킵니다. 

C++ Runtime라이브러리와 스레드

 C RunTime 라이브러리가 처음 개발된 시점에는 멀티 스레드라는 개념이 도입되기 전이라서 멀티 스레드 환경에

 전혀 고려되지 않게 디자인 되었습니다. (전역 변수를 사용하는 문제!)

 멀티 스레드 기반의 환경에서 안전한 C/C++ 런타임 라이브러리 함수를 지원하려면 각 스레드별로 전용의 

 데이터 저장소가 필요하고 C/C++ 런타임 라이브러리를 호출한 스레드는 자신의 저장소에만 접근해야 합니다. 

 하지만 OS는 새로운 스레드가 생성 될 때 C/C++로 개발된 어플리케이션인지 멀티스레드 환경인지 알지 못합니다.

 그러므로 C/C++ 어플리케이션 개발자는 스레드 생성시에 CreateThread 함수 대신에 C/C++ 런타임 라이브러리가 

 제공하는 _beginthreadex 함수를 호출해서 스레드를 생성해야 합니다. 

// Thread에 전달할 매개 변수입니다.
DWORD dwArg = 10;
// Thread ID를 받아 옵니다.
DWORD dwThreadID;
HANDLE hThread[2] = {0};
hThread[0] = CreateThread(NULL, 0, ThreadFunc, (PVOID)&dwArg, 0, &dwThreadID);
//CreateThread => _beginthreadex로 변환될 수 있습니다. 
hThread[1] = (HANDLE)_beginthreadex(nullptr, 0, (_beginthreadex_proc_type)(ThreadFunc), (void*)&dwArg, 0, (unsigned*)&dwThreadID);
// 스레드가 끝날때까지 대기 합니다.
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
if (hThread[0] != NULL) CloseHandle(hThread[0]);
if (hThread[1] != NULL) CloseHandle(hThread[1]);

 

_beginthreadex 내부 호출 _beginthreadex 함수 호출시 동작

  • 각 스레드는 C/C++ 런타임 라이브러리 힙에 _tiddata 메모리 블록을 할당합니다. 
  • _beginthreadex로 전달 받은 진입점 함수와 매개변수를 _tiddata 메모리 블록의 _initaddr, _initarg에 저장합니다.
  • WINDOW API인 CreateThread 함수를 호출하여 수행 함수를 _threadstartex로 매개변수를  _tiddata 메모리 블록을 전달하여 수행합니다. 

_threadstartex 함수 호출시 동작

  • 매개변수로 전달 받은 _tiddata 메모리 블록을 Tls(Thread Local storage)에 저장합니다. 
  • _callthreadstartex 를 호출합니다.

_callthreadstartex함수 호출시 동작 

  • C 런타임 에러와 signal 함수가 정상 동작하도록 설정 합니다.
  • _tiddata 메모리 블록에 저장한 진입점 함수와 매개변수인 _initaddr에 _initarg를 매개변수로 호출 합니다. 
  • _initaddr가 리턴되면 리턴값을 매개변수로 _endthreadex를 호출 합니다.

_endthreadex 함수 호출시 동작 

  • _tiddata 메모리 블록을 가져와서 메모리를 삭제합니다.
  • ExitThread 함수를 호출하여 스레드를 종료 시킵니다.

C런타임 함수 호출시 동작 

 만약에 표준 C/C++ 함수인 errno을 호출하게 되면 TLS에 저장된 _tiddata 구조체의 _terrno값을 가져와서 

 리턴합니다. 이렇게 전역변수가 아닌 스레드에 할당된 값을 가져와서 처리하기 때문에 멀티 스레드 동작에 

 안전한 C 런타임 함수를 지원합니다.