본문 바로가기
프로그래밍/Windows API

WIN32 쓰레드 (WINDOWS THREAD)

by 베리베리 2009. 7. 11.

출처 : Devpia, 김연기님
쓰레드(Thread)


프로세스가 실행이 되면 쓰레드가 실행 파일의 엔트리 포인트에서부터 프로그램을 실행 시킵니다.

쓰레드는 커널 오브젝트와 주소공간, 두 개의 구성요서를 가집니다.

Ø  커널 오브젝트 : 시스템이 쓰레드를 실행 하고 관리할 수 있도록 쓰레드의 정보를 가지고 있습니다.

Ø  주소공간(Address Space) : 프로그램 관점에서 볼 때 우리는 변수는 힘, 스택(지역변수), 전역공간(전역변수)에 저장이 된다고 배웠습니다.

주소공간은 스택을 말하는 것이고, 스택에는 지역 변수와 쓰레드의 실행코드가 들어 있습니다.

 

쓰레드의 실행과정


프로세스가 서브시스템에 로드 되고 쓰레드가 실행이 될 때는 커널에서 쓰레드 우선 순위에 따라 쓰레드를 실행 시킵니다.

커널에서 쓰레드를 우선 순위에 따라 쓰레드를 실행하는 작업을 쓰레드 스케쥴링(thread scheduling)이라고 하며, 커널에서 이 작업을 하는 모듈을 디스패쳐(dispatcher)라고 합니다.

디스패쳐(dispatcher)는 쓰레드의 우선 순위를 확인하고, 아래의 7가지 특성을 확인하고 실행을 합니다.

**영어를 한글로 번역하다 보니 공통적인 의미가 있습니다.

한글이름은 제가 임의로 번역한 것이지 공용적으로 사용되는 의미는 아닙니다.

영어로 알아 두시는 편이 좋을 것입니다.             

Ø  초기화(Initialized) : 쓰레드가 생성된 상태입니다.

Ø  준비(Ready) : 쓰레드가 실행되기를 기다리는 상태 입니다. 디스패쳐

dispatcher)는 우선순위를 확인하고 준비(ready)상태인 쓰

레드를 먼저 실행 합니다.

Ø  실행(Running) : 현재 쓰레드와 이전 쓰레드의 컨텍스트의 교환(Context

  Switch)() 완료 되어 쓰레드의 실행에 들어간 상태.

Ø  대기(Standby) : 대기(Standby)상태는 해당 쓰레드가 특정 CPU에 다음으

로 실행될 쓰레드를 대기(Standby) 상태라고 합니다.

Ø  기다림(Waiting) : I/O작업을 하면서 동기화 오브젝트에 의해 일시중지

(Waiting) 상태. 즉 해당 IO가 완료될 때까지 기다리고

있는 상태를 말합니다.

Ø  변환(Transition) : 쓰레드가 실행되기 전 상태. 준비(Ready)상태와의 차이

점은 커널 스택에 쓰레드 프로시져(Procedure) 코드 영

역이 올라온 상태는 준비(Ready)상태이고, 올라오지 않

은 상태는 변환(Transition)상태입니다.

Ø  종료(Terminated) : 쓰레드의 실행이 종료된 상태입니다.

 

아래 그림은 쓰레드의 생성->실행->종료까지 나타낸 그림입니다.

숫자는 실행되는 순서를 나타냅니다.

그림 출처 : Windows Internals 4th

 

쓰레드의 사용


Win32 API에서 쓰레드를 생성하는 함수는 CreateThread()함수와 _beginthrea

d 가 있습니다.

MFC에서는 AfxCreateThread를 사용할 수 있습니다.

 

CreateThread


HANDLE WINAPI CreateThread(

  __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,

  __in       SIZE_T dwStackSize,

  __in       LPTHREAD_START_ROUTINE lpStartAddress,

  __in_opt   LPVOID lpParameter,

  __in       DWORD dwCreationFlags,

  __out_opt  LPDWORD lpThreadId

);

lpThreadAttributes : SECURITY_ATTRIBUTES구조체의 포인터를 입력하는 인자입니다.

 

dwStackSize : 쓰레드 스택 사이즈를 설정합니다.  0으로 두면 쓰레드 스택사이즈는 시스템 가상메모

범위내에서 정해집니다. 기본으로 설정된 사이즈는 1Mbyte입니다.

http://msdn2.microsoft.com/en-us/library/ms686774(VS.85).aspx

 

lpParameter : 쓰레드 프로시져 함수에 넘겨줄 데이터를 입력하는 인자입니다.

dwCreationFlags : 쓰레드를 생성된 상태를 설정합니다. CREATE_SUSPEND 설정하면

 ResumeThread() 호출하기 전까지 동작하지 않습니다. 0으로 주면 즉시 실행

하게 됩니다.

lpThreadId  : 쓰레드 ID 입력 받는 인자입니다.

http://msdn2.microsoft.com/en-us/library/ms682453.aspx

Return : 쓰레드 생성에 성공하면 쓰레드의 핸들을 리턴 합니다.

 

SECURITY_ATTRIBUTE 해당 프로세스/쓰레드/파일 등을 생성할 외부로부터 접근할 권한을 설정하는 역할을 합니다.

typedef struct _SECURITY_ATTRIBUTES {
  DWORD nLength;
  LPVOID lpSecurityDescriptor;
  BOOL bInheritHandle;

} SECURITY_ATTRIBUTES,
 *PSECURITY_ATTRIBUTES,
 *LPSECURITY_ATTRIBUTES;

nLength : SECURITY_ATTRIBUTES 구조체의 사이즈

lpSecurityDescriptor : 오브젝트의 권한을 설정하는 구조체입니다.

SECURITY_DESCRIPTOR구조체를초기화 하고 설정하는 방법은 다음 링크를 참조하세요.

http://msdn2.microsoft.com/en-us/library/aa446595(VS.85).aspx

bInheritHandle  : TRUE이면 상속 받을수 있습니다.

http://msdn2.microsoft.com/en-us/library/aa379560(VS.85).aspx

 

CreateThread로 생성된 쓰레드는 CloseHandle로 종료 시킵니다.

 

_beginThread/_beginthreadex


uintptr_t _beginthread(

   __in void( __cdecl *start_address )( void * ),    /*쓰레드 프로시져(Procedure 함수*/

   __in_opt unsigned stack_size,  /*쓰레드 스택 사이즈 기본 1Mbyte*/

   __in_opt void *arglist   /*쓰레드 프로시져에 인자로 넘길 데이터*/

);

uintptr_t _beginthreadex(

   __in_opt void *security,/*SECURITY_ATTRIBUTE 구제체 포인터*/

   __in_opt unsigned stack_size, /*스택 사이즈*/

   __in unsigned ( __stdcall *start_address )( void * ), /*쓰레드 프로시져*/

   __in_opt void *arglist, /*쓰레드 프로시져에 넘길 데이터*/

   __in_opt unsigned initflag, /*초기 상태 설정*/

   __out_opt unsigned *thrdaddr /*쓰레드 ID*/

);

Beginthread/beginthreadex 함수의 인자는 CreateThread와 비슷합니다.

Beginthread/beginthreadex 함수를 사용하여 만든 쓰레드는 CloseHandle을 함수로 종료 시킵니다.

그러나 쓰레드 프로시져는 계속 실행되고 있음을 유의해야 합니다.

이를 방지하기 위해서는 프로시져 함수내에서 플래그를 두어 쓰레드 프로시져가 리턴을 하던지 _endthread/_endthreradex 함수가 호출이 되어야합니다.

다음 예제는 _beginthreadex로 쓰레드를 생성하여 쓰레드 사용이 필요없을 때

CloseHandle을 호출하고 프로시져내에서 쓰레드를 안전하게 종료하는 예입니다.

 

unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler)

{

        while (bExit == FALSE) /*쓰레드가 종료 될 때 bExit TRUE로 두어 빠져나오게합니다*/

        {

                //뭔가를 합시다.(Do Something -_-;;)

        }

/*_endthreadex함수를 호출하여 쓰레드가 정상적으로 종료 되게 합니다.*/

        DWORD exitCode;

        GetExitCodeThread(InputThrd, &exitCode);

        _endthreadex(exitCode);

        return 0;

}

 

_beginthread로 시작한 쓰레드는 반드시 _endthread로 종료하고,

_beginthreadex 로 시작한 쓰레드는 _endthreadex로 종료 시켜야합니다.

, 짝을 맞춰주어 종료 시켜야 안전하게 종료 됩니다.

 

CreateThread함수와 BeginThread/BeginThreadEx의 중요한 차이점이 있습니다.

Createthread로 생성된 쓰레드 내에서는 CRT함수를 쓰면 메모리 릭이 발생하고,

프로세스가 아무런 에러 리포팅이나 경고 없이 종료 되는 현상이 발생 할 수

있습니다.

자세한 내용은 Windows Via C/C++ 이나 Jeffrey책을 참조 하시길 바랍니다.

AfxBeginThread


MFC에서 쓰레드는 AfxCreateThread를 이용하여 쉽게 만들어 질수 있습니다.

AfxCreateThread  아래와 같이 선언되어있습니다.

 CWinThread* AfxBeginThread(

   AFX_THREADPROC pfnThreadProc,

   LPVOID pParam,

   int nPriority = THREAD_PRIORITY_NORMAL,

   UINT nStackSize = 0,

   DWORD dwCreateFlags = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

CWinThread* AfxBeginThread(

   CRuntimeClass* pThreadClass,

   int nPriority = THREAD_PRIORITY_NORMAL,

   UINT nStackSize = 0,

   DWORD dwCreateFlags = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

CreateThread _beginthread와 비슷한 인자를 가지 므로 따로 설명은 하지 않겠습니다.

두번째 선언에서 CRuntimeClass* 타입의 인자를 받습니다. CWinThread클래스를 상속 받아 UI를 가지는 쓰레드를 생성할 수 있습니다.

쓰레드를 종료할 때는 프로시져 함수에서 플래그 변수를 두어 종료 할수 있게 하고 AfxEndThread함수를 호출하거나 리턴을 하면 정상적으로 종료가 됩니다.

 

세가지 쓰레드 함수의 차이점

앞에서 본 것과 같이 Win32 API /MFC에서는 세 가지 함수를 이용해서 쓰레드를 생성할 수 있습니다.

이 세가지 쓰레드의 차이점은 CRT 함수를 안전하게 쓸 수 있는가 없는가에 차이가 있습니다.

결론을 말하자면 CreateThread CRT함수를 쓰면 메모리릭이나 프로그램의 비정상적인 종료 같은 문제를 발생 시킵니다.

_beginthread AfxBeginThread함수는 CRT 함수를 안전하게 쓸수 있습니다.

AfxBeginThread함수는 내부적으로 _beginthreadex함수를 호출합니다.

아래의 코드는 AfxBeginThread함수 코드입니다.

CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,

        int nPriority, UINT nStackSize, DWORD dwCreateFlags,

        LPSECURITY_ATTRIBUTES lpSecurityAttrs)

{

#ifndef _MT

        pfnThreadProc;

        pParam;

        nPriority;

        nStackSize;

        dwCreateFlags;

        lpSecurityAttrs;

 

        return NULL;

#else

        ASSERT(pfnThreadProc != NULL);

 

        CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);

        ASSERT_VALID(pThread);

        //CWinThread::CreateThread함수를 호출하여 _beginthreadex를 호출합니다.              

        if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,

             lpSecurityAttrs))

        {

               pThread->Delete();

               return NULL;

        }

        VERIFY(pThread->SetThreadPriority(nPriority));

        if (!(dwCreateFlags & CREATE_SUSPENDED))

               VERIFY(pThread->ResumeThread() != (DWORD)-1);

 

        return pThread;

#endif //!_MT)

}

 

 

 

BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize,

        LPSECURITY_ATTRIBUTES lpSecurityAttrs)

{

#ifndef _MT

        dwCreateFlags;

        nStackSize;

        lpSecurityAttrs;

 

        return FALSE;

#else

        ENSURE(m_hThread == NULL);  // already created?

 

        // setup startup structure for thread initialization

        _AFX_THREAD_STARTUP startup; memset(&startup, 0, sizeof(startup));

        startup.pThreadState = AfxGetThreadState();

        startup.pThread = this;

        startup.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);

        startup.hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL);

        startup.dwCreateFlags = dwCreateFlags;

        if (startup.hEvent == NULL || startup.hEvent2 == NULL)

        {

               TRACE(traceAppMsg, 0, "Warning: CreateEvent failed in CWinThread::CreateThread.\n");

               if (startup.hEvent != NULL)

                       ::CloseHandle(startup.hEvent);

               if (startup.hEvent2 != NULL)

                       ::CloseHandle(startup.hEvent2);

               return FALSE;

        }

 

        // create the thread (it may or may not start to run)

        //_beginthreadex함수를 호출합니다.

        m_hThread = (HANDLE)(ULONG_PTR)_beginthreadex(lpSecurityAttrs, nStackSize, 

             &_AfxThreadEntry, &startup, dwCreateFlags | CREATE_SUSPENDED, (UINT*)&m_nThreadID);

        if (m_hThread == NULL)

               return FALSE;

 

        // start the thread just for MFC initialization

        VERIFY(ResumeThread() != (DWORD)-1);

        VERIFY(::WaitForSingleObject(startup.hEvent, INFINITE) == WAIT_OBJECT_0);

        ::CloseHandle(startup.hEvent);

 

        // if created suspended, suspend it until resume thread wakes it up

        if (dwCreateFlags & CREATE_SUSPENDED)

               VERIFY(::SuspendThread(m_hThread) != (DWORD)-1);

 

        // if error during startup, shut things down

        if (startup.bError)

        {

               VERIFY(::WaitForSingleObject(m_hThread, INFINITE) == WAIT_OBJECT_0);

               ::CloseHandle(m_hThread);

               m_hThread = NULL;

               ::CloseHandle(startup.hEvent2);

               return FALSE;

        }

 

        // allow thread to continue, once resumed (it may already be resumed)

        ::SetEvent(startup.hEvent2);

        return TRUE;

#endif //!_MT

}

 

그렇다면 CreateThread함수와 _beginthread 함수의 차이점이 무엇인지 궁금할 것입니다.

_beginthread 함수도 코드를 보면 CreateThread  함수를 호출합니다.

이유는 CreateThread함수는 커널이 쓰레드가 생성되었다는 것을 알수 있는 유일한 방법이기 때문입니다.

차이점은 _beginthread는 커널에 CRT함수들을 위한 데이터 공간을 주어서 안전하게 CRT 함수를 사용할 수 있도록 합니다.

**자세한 사항은 Windows Programming Via C/C++ 이나 Jeffrey책을 참조 하세요.

 

진짜 메모리릭이 발생하는지 테스트를 해보았습니다.

다음은 테스트 코드입니다.

프로젝트는 콘솔모드로 만들었고 사용자 입력을 받는 쓰레드와 CRT함수를 사용하는 쓰레드 두개를 사용합니다.

테스트를 위해서 CreateThread 프로시져와 _beginthreadex 프로시져 각각 따로 만들고 번갈아 가면서 테스트를 수행했습니다.

디버깅 툴은 IMB사의 Purify Plus를 사용했습니다.

BounceChecker를 많이 사용하시는데, Purify Plus는 코드에서 어떤 함수를 호출해서 메모리릭이나 에러가 났는지 정확하게 표시해주는 장점이 있습니다.

 

#include <Windows.h>

#include <iostream>

#include <istream>

#include <process.h>

 

//CreateThread 프로시져 함수

DWORD WINAPI ThreadProc(__in  LPVOID lpParameter);

DWORD WINAPI InputProc(__in  LPVOID lpParameter);

         

//_beginthreadex 프로시져 함수

unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler);

unsigned _stdcall InputProc_BT(void *pThreadHandler);

 

 

BOOL bExit = FALSE;  //쓰레드 종료를 위한 플래그 변수

HANDLE ThrdHandle=0;

HANDLE InputThrd = 0;

DWORD thrdID;

int _tmain(int argc, _TCHAR* argv[])

{

        unsigned int ID1, ID2;

        //CreateThread를 이용해서 쓰레드 생성

        ThrdHandle = CreateThread(0,0, ThreadProc, 0, 0, &thrdID);

        InputThrd = CreateThread(0,0, InputProc, 0, 0, &thrdID);

       

//_beginthreadex 함수를 사용할 때 아래의 주석을 빼고 위의 CreateThread부분을 주석

//      ThrdHandle  = reinterpret_cast<HANDLE>(_beginthreadex(0,1024,CallThreadHandlerProc,0,0,&ID1));

//      InputThrd = reinterpret_cast<HANDLE>(_beginthreadex(0,1024,InputProc_BT,0,0, &ID2));

       

        WaitForSingleObject(ThrdHandle, INFINITE);

 

        CloseHandle(ThrdHandle);

        CloseHandle(InputThrd);

        return 0;

}

 

DWORD WINAPI ThreadProc(__in  LPVOID lpParameter)

{

        while (bExit == FALSE)

        {

               int  decimal, sign;

               char *buffer;

               double source = 3.1415926535;

                //CRT함수 호출

               buffer = _fcvt( source, 7, &decimal, &sign );

        }

       

        return 0;

}

 

DWORD WINAPI InputProc(__in  LPVOID lpParameter)

{

        char ch;

        std::cin>>ch;

       

bExit = TRUE;

       

        return 0;

}

 

unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler)

{

        while (bExit == FALSE)

        {

               int  decimal, sign;

               char *buffer;

               double source = 3.1415926535;

                //CRT함수 호출

               buffer = _fcvt( source, 7, &decimal, &sign );

        }

        DWORD exitCode;

        GetExitCodeThread(InputThrd, &exitCode);

        _endthreadex(exitCode);

        return 0;

}

 

unsigned _stdcall InputProc_BT(void *pThreadHandler)

{

        char ch;

        std::cin>>ch;

        bExit = TRUE;

       

        DWORD exitCode;

        GetExitCodeThread(ThrdHandle, &exitCode);

        _endthreadex(exitCode);

        return 0;

}

 

 

CreateThread로 쓰레드를 만들었을 때 결과.

 

_beginthread로 쓰레드를 만들었을 때 결과

 

 

 

결론


쓰레드를 만드는데 CreateThread _beginthread함수가 있습니다.

MFC에서 쓰레드를 쉽게 만들려면 AfxBeginThread를 사용할수 있고, AfxBeginThread에서는 특별히 UI를 가지는 쓰레드를 만들 수 있습니다.

쓰레드 프로시져 내에서 CRT함수를 쓸 때 는 _beginthread 로 쓰레드를 만들어 안전하게 CRT함수를 쓸수 있습니다.

** CRT함수 라이브러리 변수에는 errno, _doserrno, strok, _wcsok, strerror, _strerror, tmpnam, tmpfile, asctime, _wsctime, gmtime, _evct, _fcvt 가 있습니다. 이상은 Jeffrey책의 내용이었구요, 제생각에는 C표준 함수 몇 개 더 있는것 같습니다.


참고 서적 : Windows Interanals 4th Edition

Windows Via C/C++

 

PS


메모리릭은 간단한 테스트로 확인 할 수 있었지만 프로그램이 갑자기 죽는 경우는 테스트 할수 없었습니다.

예전에 그런 경우가 있었는데 그게 CreateThread때문인지 OCX때문인지 원인을 알 수는 없었습니다.

원래 쓰레드 프로시져를 클래스로 만들어 쉽게 사용할 수 있는 방법에대해 쓸려고 했는데 쓰다보니 내용이 많아 졌습니다.

다음엔 동기화 오브젝트와 쓰레드 프로시져 클래스 설계 하는것에 대해 강좌(?)를 만들겠습니다.

긴 글 읽어주셔서 감사합니다.

리액션좀 주세요~~~T..T

댓글