본문 바로가기
프로그래밍/MFC

쓰레드

by 호군 2010. 10. 27.
반응형
- Thread -
 
  귀가 닳도록 들어온 워커스레드와 UI스레드. 그리고 스레드와 관련된
 숱한 의혹과 혼란은 간혹 프로그래밍 할 맛을 뚝 떨어뜨리기도 한다.
 그러나 세상에 좋은걸 쉽게 얻을수 있는게 어디 있으랴. 보다 좋은 성능에
 보다 매끄러운 결과물을 만들기 위해서는 피해갈수 없는 길이 스레드다.
 그만큼 잘 쓰면 프로그램을 빛나게 하지만 잘못쓰면 그냥 망한다.
 그리고 알아야 할것이 한두가지가 아니다. 이렇게 복합적인 지식을 요구하기
 때문에 스레드가 어렵게 느껴질수 있다.
 적어도 멀티스레드로 들어가기 시작하면...

 그래서 기초책에서 설명하는 그렇고 그런 진부한 내용은 다 집어치우고,
 또 잘 설명해 주지도 않는 UI 스레드도 제대로 알아볼겸
 MFC에서 스레드가 어떻게 동작하는지 MFC 소스코드를 살펴보고
 멀티스레드를 운용할때 무엇을 조심해야하는지 알아보고,
 공유섹션을 어떻게 보호하며 성능을 최대한 끌어 올릴수 있는지
 알아보도록 하자.


-----------------------------------------------------------------------
 * CWinThread.
 

 MFC에서 스레드를 생성하는 함수는 두가지가 있다. 잘 알다시피
 AfxBeginThread() 와 CWinThread::CreateThread() 이다.
 이 둘은 어떻게 다른것일까?
 그리고 ::CreateThread() 나 _beginthreadex() 는 쓸수 없는것인가?

 원래 스레드를 생성하는 함수는 WIN32 api 인  ::CreateThread()와 _beginthread,ex
() 밖에 없다.
 그런데 MFC 클래스로 스레드 사용을 객체화 시킨것이 CWinThread 이다.
 그리고 CWinThread 는 스레드를 생성하기 위해 _beginthreadex()를 호출한다.

 AfxBeginThread()는 워커스레드 혹은 UI 스레드를 보다 쓰기쉽게 만든 범용함수라 할
수있다.
 AfxBeginThread()의 두가지 버전중에서 스레드 함수 포인터를 넘겨주는 버전은
 CWinThread 와 별로 상관이 없을것이라는 착각을 하기 쉬운데 그렇지 않다.
 AfxBeginThread() 는 CWinThread 를 내부적으로 사용하고 있다. 물론 두가지 버전
 모두에서다. 이같은 사실은 AfxBeginThread() 가 리턴하는 것이 CWinThread 포인터
 라는데서 쉽게 짐작할수 있다. 또 스레드를 구현한 MFC 소스코드를 살펴봄으로써 
 보다 자세히 알수 있다.

 MFC 소스코드중 thrdcore.cpp 이란 파일에서 우리는 CWinThread 의 소스코드를 볼수
있다.
 먼저 AfxBeginThread() 를 찾아보자.

 첫번째로 
 CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,
    int nPriority, UINT nStackSize, DWORD dwCreateFlags,
    LPSECURITY_ATTRIBUTES lpSecurityAttrs) 
 {
 ....
         CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam);

 정확히 166라인에 AfxBeginThread() 가 CWinThread 객체를 생성하는 것을 볼수있다. 
 그리고 조금아래에서 

        if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize,
        lpSecurityAttrs))
        {
              pThread->Delete();
              return NULL;
    }

 와 같이 생성한 CWinThread 객체의 메소드인 CreateThread() 를 호출해서 스레드를
 생성는데 CREATE_SUSPENDED 로 실행이 중지된 스레드를 생성한다.
 그리고 아래에서 AfxBeginThread() 의 파라미터로 전달한 우선순위로 변경한뒤
 역시 파라미터로 전달한 dwCreateFlags 에 맞게 스레드를 Resume 시킬것인지를 결정
한다.
 이와 같은 일련의 과정은 우리가 손수 CWinThread 객체를 생성해서 사용하는 일반적

 방법과 정확히 일치하며 CWinThread 객체를 생성하는 가장 정석의 방법이다.

 바로 아래에 나오는 두번째 AfxBeginThread() 역시 첫번째와 별 틀린점이 없다는 것

 또한 볼수 있다.

 그러면 CWinThread 를 사용자가 그냥 쓰면 되지 왜 AfxBeginThread() 가 있는가?
 그에 대한 해답은 MFC 클래스를 만든 MS의 개발자들이 해야 겠지만 적어도 내가 보기

 특별한 이유는 없다. 
 단지 CWinThread 객체를 생성하고 스레드를 시작시키는 일련의 작업을 보다 쉽게 처

 하기 위해 있을 뿐이다. 편의를 위해 생긴 함수랄까? AfxMessageBox() 와 
::MessageBox()처럼.

 그렇지만 AfxBeginThread() 를 쓰는것은 일반적으로 지극히 권장할만 하다.
 그것은 AfxBeginThread()는 프로그래머가 수고해야할 여러가지 작업들을 대신해 주고
 있기 때문이다. 그것은 ASSERT_VALID()와 같이 보다 코드의 위험성을 줄일수 있도록
 하는것들과 발생할지 모르는 메모리 예외오류 등을 매끄럽게 처리하고 있다.
 만약 여러분이 CWinThread 를 그냥 쓴다고 하면 AfxBeginThread() 가 처리해 주는 이

 일들에 소홀할지 모르고, 그것은 단순히 편리함 이상의 안전함을 포기하는 것과 같
다.
 시작이 있으면 끝도 있는법. AfxBeginThread()와 쌍을 이루는 AfxEndThread() 도 있
다.
 그러나 쓸일은 거의 없다고 본다.

 *참고*
 코드에 _MT 라는 매크로가 자주 등장하는 것을 보는데 이는 컴파일러 옵션에서 멀티
스레드
 를 지원하도록 할때 정의되는 매크로다. 디폴트로 _MT 는 정의가 되므로 걱정하지 않
아도
 되고 이는 Project->Setting->C/C++ 탭의 Code Generation 카테고리에서 볼수있다.


 이제 CWinThread의 소스코드를 보도록 하자. 조금 아래로 내려가면 CWinThread의
 소스코드를 볼수있다. 앞서 설명했지만 CWinThread 는 _beginthreadex()를 호출해서
 스레드를 실행한다.
 
 CWinThread의 생성자에서 볼수있는 흥미로운 점은 m_pMainWnd 라는 멤버를 가지고
 있다는 것이다. 위자드를 통해 어플리케이션을 만들때 CWinApp 에서 상속을 받는것으

 시작한 것을 기억하는가? 그 CWinApp 는 CWinThread에서 상속받았다. 
 따라서 메세지펌핑이 CWinThread 클래스에 존재하는것은 당연한 것이다.
 특히 UI 스레드를 사용하는데 있어 CWinThread 가 메세지 루프를 돌린다는 점은 매
우 당연
 한 사실이다. UI스레드는 메세지 루프를 가지는 스레드로써 워커스레드와 구별된다.
 간단히 이야기하자면(실제로는 복잡한 이야기지만) UI 스레드는 워커스레드에 메세지
펌핑
 이 첨가된 스레드의 형태라고 할수있다. 싫든좋든 하나의 프로세스로 이루어진 어플
리케이션
 이 최소한 하나의 스레드를 가진다고 말할수 있는것은 곧 그것이 UI스레드 이기 때문
이다.
 만약 MFC가 UI 스레드를 만들지 않았다면 여러분은 UI 스레드 같은 클래스를 구현하
기 위해
 워커스레드에서 메세지 펌핑 및 윈도우프로시저를 구현하기 위한 복잡한 작업들을 손

 해야 할것이다. 그런데 UI스레드가 이미 CWinThread로 구현되어 있으니 그 얼마나 다
행스러운가.

 자. 다시 본래 이야기로 돌아가자.
 
 CWinThread 에서 스레드를 생성시키는 CWinThread::CreateThread() 를 찾아라.
 AfxBeginThread()에서 잠시 보았던 CWinThread::CreateThread()의 소스코드가 주루
룩 나오는가?
 그중에서 스레드를 생성하는 루틴이 있는 408라인 _beginthreadex().
 앞서 이야기 한것처럼 CWinThread() CWinThread::CreateThread()에서 _beginthreadex
()를
 호출하여 새로운 스레드를 생성한다. 그리고 생성된 스레드는 CWinThread의 멤버인 
 m_hThread 에 집어넣는 것을 직접 볼수있다. 그리고 그게 모두이다. That"s all.
 Event 객체들이 왜 나오지? 하고 궁금증을 갖는 사람이 있을 것이다. 소스코드를 더 
읽어
 보면 알겠지만 _beginthread()에 넘겨주는 스레드 프로시저 함수의 포인터가 무엇인

 잘보면 안다. 그것은 우리가 지정한 프로시저 포인터가 아니고 _AfxThreadEntry() 라
는 
 함수의 포인터다. _AfxThreadEntry() 가 뭐일까? 
 갑자기 이놈이 나온 이유는?
 CWinThread 가 워커로 생성되거나 혹은 UI로 생성된다면 분명 하는 작업이 틀리겠지.
 워커스레드가 InitInstance()가 실행될리 만무하지 않는가. 물론 Run() 이나 
ExitInstance()도.
 _AfxThreadEntry()는 CWinThread 가 생성하는 스레드를 안전하게 생성되게 하고 메세

 루프를 돌리기 위한 준비를 갖추고 또 스레드가 가져야할 여러가지 속성 이나 메인 
윈도우
 와의 연결을 처리한다.
 _AfxThreadEntry() 는 44라인에 소스코드가 있다. 이체로운 점은 CWnd 객체를 하나 
사용
 한다는 점과 이벤트객체가 사용된다는 점이다. 
 그렇다.
 약간 이상하다고 생각할지 모르지만 CWinThread 에서는 워커든 UI든 CWnd 객체가 사
용된다.
 그것은 m_pMainWnd, 즉 메인 윈도우가 없다면 이 스레드가 메인윈도우가 되기 위해서
다.
 CWinApp 가 CWinThread에서 상속된다는 사실을 항상 명심하라.
 이벤트 객체는 스레드를 생성한 CreateThread() 가 실제로 스레드가 온전하게 실행되
었다는
 것을 확인하기 위한 동기를 맞추기 위해 필요하다.
 _AfxThreadEntry() 는 이미 스레드 프로시저다. 이미 실행되고 있는 스레드 함수다.
 그러나 아직 우리가 지정한 스레드 함수는 실행한적도 없다. 그 이전에 
_AfxThreadEntry()
 가 각종 선행작업을 모두 마무리 할때 까지 CreateThread() 는 결과를 지켜볼수 있도

 하는 것이다. 이런 선행작업이 비로소 마무리 되었을때 CreateThread() 는 성공적으
로 
 스레드가 시작되었다고 판단하고 반대의 경우는 실패 했다고 본다.
 110 라인을 보면 이제서야 우리가 지정한 스레드 프로시저가 실행됨을 볼수있다.
 그것이 워커라면 그냥 함수 하나 달랑 실행하는 것이 될것이고 UI 스레드라면
 InitInstance() 를 실행하는 것을 볼수 있다. 만약 InitInstance()가 가상함수로
 자식 클래스에서 구현하지 않는다면 CWinThread::InitInstance()는 무엇을 리턴하는
가?
 FALSE 다. 그러면 무조건 스레드가 무조건 종료할수 밖에 없다. 그래서 InitInstance
()
 는 반드시 리턴해서 초기화 작업을 수행토록 하고 성공적일 경우 TRUE를 리턴시켜야 
한다.
 그래야만 믿에서 드디어 Run() 이 실행되고 WM_QUIT 이 들어올때 까지 메시지 루프를
 돌게 되는 것이다. 이게 CWinThread 객체를 만들면 무조건 위자드가 InitInstance()

 오버라이드 시키는 이유이다.
 둘다 실행을 마친뒤는 _AfxThreadEntry()의 끝에 치닫는다. 즉 _AfxThreadEntry()가 
 리턴함으로써 실제로 스레드가 종료되게 되는 것이다.


 이제 진짜 중요한 부분. CWinThread의 핵심. CWinThread::Run() 을 보자.
 대게 CWinThread의 오버라이드 가능한 Run()에 대한 쓰임세에 혼동하는 사람들이 많
은데
 그것은 아마도 함수의 이름때문에 그런것 같다.
 UI 스레드와 워커스레드(이것은 마치 함수 하나를 비동기적으로 실행하는 것)의 차이

 모른체 워커스레드만 쓰다가 UI스레드를 접할때 생기는 혼동으로 Run 이 워커스레드

 프로시저 함수 본체 쯤으로 생각하는 오해이다. 뭐 코드로 보면 이렇게..

 UINT MyThreadFunc( void *p )
 {
      CWinThread::Run()
 }

 -_-;

 아니다. 아니다. 이게 아니다.
 CWinThread::Run() 은 워커스레드에서 처럼 무엇을 실행하기 위해 오버라이드 가능한
 메소드가 아니라 메세지 루프를 돌리는 메소드다. UI스레드를 쓸때에도 특별한 경우

 제외하고는 거의 오버라이드 할 일도 없다. 또 실제로 Run을 오버라이드 하고 디폴트
 CWinThread::Run() 를 막고 거기서 while()을 돌려 워커스레드 처럼 쓰는 경우도 있
는데
 그럴바에야 워커스레드를 쓰는게 낫다.
 그럼 이놈이 도대체 무슨일을 하기에 이름도 거창한 Run()이 되었는지 살펴보자.

 // main running routine until thread exits
 Run()을 설명한 주석이 벌써 모든것을 말하는군.
 스레드가 끝날때 까지 돌고도는 함수. 메세지 루프를 돌려야 하니깐 돌지.
 흥미로운 부분은 

 // phase1: check to see if we can do idle work
 while (bIdle &&
     !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
 {
         // call OnIdle while in bIdle state
         if (!OnIdle(lIdleCount++))
              bIdle = FALSE; // assume "no idle" state
 }

 이 부분이다.


 check to see if we can do idle work  !

 idle working 을 할수 있는지 체크하고 가능하면 idle working 을 하라!
 PeekMessage() 로 먼저 주어진 메세지큐에서 메세지를 살펴보고 메세지가
 하나도 없으면 OnIdle() 를 호출하라.
 즉 처리해야할 메세지가 없으면 노느니 할일 있으면 해라 라는 뜻이다.
 재미있는점은 lIdleCount 가 증가하는 것과 OnIdle()가 FALSE 를 리턴하면
 루프를 빠져나간다는 점이다.

 OnIdle() 은 메세지가 없을때 반복적으로 호출될수 있는 오버라이드 가능한
 메소드임을 알았다. 그런데 lIdleCount는 왜 증가 시켰을까?
 그것은 528라인의 CWinThread::OnIdle(LONG lCount) 소스코드를 보면 이해할수 있다.
 OnIdle() 의 설명은 책에 다 나오는 내용이니깐 뭐 더 설명할것 없겠고..

 다시 소스코드로 돌아오자.
 OnIdle() 작업이 끝나면 예상대로 메세지 펌핑을 한다. 메세지를 가져와서 WM_QUIT
 가 아니면 윈도우프로시저를 호출해서 메세지 핸들러들을 수행하게 하고 메세지가
 없을때 까지"while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))"
 계속 펌핑을 한다. 그러다 메세지가 없으면 또 OnIdle() 처리를 하겠지.

 그런데 UI스레드를 쓰는 목적은 OnIdle() 에서 끄적거릴려고 쓰는게 아니다.
 UI스레드의 핵심은 메세지다. 메세지를 쓰기위한게 주 목적이고 OnIdle()은 그야말로
 별책부록이다. 
 우리는 PostThreadMessage() 를 통해 특정 UI 스레드에 메세지를 보낼수 있다.
 실전에선 대부분 User Define 메세지를 많이 쓰게 될것이다. 이렇게 메세지를 처리하

 구조의 UI 스레드는 책에서 설명하는것 처럼 화면에 나올 UI를 처리하는 목적으론
 거의 쓸일없다. 오히려 메세지큐를 이용해 멀티스레드를 아주아주 쉽게 쓸수 있다.

 가령 멀티스레드에서 여러개의 스레드가 특정한 JOB이 생기지 않을때 CPU자원을 쓰지
 않고 휴지상태에 있어야 하고 특정 JOB이 발생할때만 처리되어야 할때 워커스레드에

 대부분 WaitFor..Object()를 이용하려고 할것이다. 그러나 UI 스레드에서는 메세지가
 없으면 자동으로 쉰다(진짜 편리하지 않는가?).
 그리고 극도의 상황에서 여러개의 JOB이 발생할때 JOB이 큐잉되어야 한다면 그것을
 메세지큐를 이용해 쉽게 큐잉할수 있다는 점이다.
 따라서 처리할 JOB을 여러 멀티스레드에게 골고루 배분하는 점만 잘 구현하면 이런
 형태의 구조를 아주 쉽게 UI 스레드로 구현할수 있을것이다.
 만약 여러분이 워커스레드로 구현한다면 폭주하는 JOB의 큐잉까지 신경써야 할텐데
 그것이 거의 퍼펙트하고 안전한 방법으로 처리할수 있는 것이다.
 그러나 물론 메세지를 이용한다는 자체가 약간 무겁기 때문에 멀티스레드 끼리의
 경합하에 JOB을 수행토록 한다면, 그것이 또 I/O의 문제라면 IOCP 를 써서 해결하면
 메세지큐를 검사하지 않기 때문에 가볍고, 커널객체인 이유로 속도가 빠르다.
 
 
 지금까지 설명하면서 CreateThread() 를 꼭 CWinThread::CreateThread() 라고 표현한
 것을 눈치챘기를 바란다.
 WIN32 API 인 ::CreateThread() 는 CWinThread::CreateThread()랑은 많이 다르다.
 CWinThread::CreateThread() 는 ::CreateThread() 와는 달리 MFC에서 사용되기 위한
 많은 부분이 첨가되고 자원에 대한 접근 및 스레드가 온전하게 생성되고 실행되도록
 검사하며 특히 C런타임 함수들이 안전하게 엑세스 되도록 보장하는 일들을 수행한다.
 벌써 CWinThread 의 긴 소스코드를 보면서 느꼈으리라 본다. ::CreateThread() 로
 어찌 지금까지 설명한 내용들을 코딩하리.. 난 게을러서 못한다..



 *스레드 종료

 스레드 종료에 대한 최고의 방법은 간단하다.
 스레드가 더이상 실행될 필요가 없을때 스스로 종료할수 있도록 하고 여러분은 
 아무런 걱정을 하지 않으면 된다. 그게 모두다. 
 또 스레드를 가진 부모 프로세스가 종료하면 스레드는 스스로 죽는다.
 그리고 우리는 OS가 스레드를 종료시키고 스레드 스택과 사용되었던 리소스가
 안전하게 재거되고 반환되었다고 믿는 수 밖에 없다.

 그러나 스레드가 교착상태에 빠지거나 스스로 빠져나올 조건이 알수없는 이유로
 걸릴수 없을때 강제로 스레드를 종료하는 방법이 존재한다.
 그러나 어떠한 방법으로든, 스레드를 강제로 종료하는 것은 최후의 수단이다.

 일단 종료시 주의할점들을 몇가지 살펴보자.

 그럴리 없다고 보지만 여러분이 직접 _beginthreadex() 로 스레드를 생성했다면
 _endthreadex()로 스레드를 종료하게 되는데 _endthread()와는 다르게 이 함수는
 스레드 핸들을 자동적으로 닫아주지 않는다. 따라서 여러분은 _endthreadex()후
 CloseHandle()을 호출할 필요가 있다. 그러나 나는 아직까지 _endthreadex()를
 호출하도록 코드를 만들어 본적이 없다. 대게의 경우 _endthreadex()를 호출하지
 않더라도 스레드가 스스로 리턴될때 스레드스택은 파괴되고 리소스는 반환된다. 믿어
라.
 어쨋든 이 부분에 대한 MSDN의 원문을 간추려 보면 아래와 같다.

 _endthread automatically closes the thread handle. 
 (This behavior differs from the Win32 ExitThread API.)
 Therefore, when you use _beginthread and _endthread, do not explicitly 
 close the thread handle by calling the Win32CloseHandle API. 

 Like the Win32 ExitThread API, _endthreadex does not close the thread handle. 
 Therefore, when you use _beginthreadex and _endthreadex, you must close 
 the thread handle by calling the Win32 CloseHandle API.

 또 유의할 사항이 있다. 이것역시 여러분이 거의 주의할 일이 없겠지만..
 만약 _beginthread() 나 _beginthreadex()로 스레드를 시작했다면 WIN32 API인
 ExitThread()를 호출하지 않는게 몸에 좋다는 것이다.
 ExitThread()는 _beginthread()와 짝이 아니다. 그건 ::CreateThread()와 짝이다.
 MSDN에서는 _beginthread,ex() 를 쓸 경우 static library 로 링크할경우 사용되는
 LIBCMT.LIB에서는 사용된 리소스가 제대로 해제되지 않을것이라고 경고하고 있다.
 원문을 첨부한다.

 Note
 For an executable file linked with LIBCMT.LIB, do not call the Win32 
ExitThread API;
 this prevents the run-time system from reclaiming allocated resources. 
_endthread and _endthreadex reclaim allocated thread resources and then call 
ExitThread.


 ::CreateThread()로 스레드를 만들었을때도 마찬가지다.
 ExitThread()는 안써도 된다. 즉 스레드가 그냥 종료될수 있도록 하면 그만이다.

 MFC의 경우 AfxBeginThread() 나 CWinThread::CreateThread() 를 이용해서 스레드를
 만들었을 때 여러분은 정말 다행스러운 선택을 한것이라고 말하고 싶다.
 적어도 위의 방법은 최대한 여러분에게 안전하게 스레드가 종료될수 있도록 하는
 작업들을 내부적으로 대신하고 있다. 여러분은 단지 m_bAutoDelete = TRUE를 
 해주는 것만으로 스레드가 리턴될때 자동으로 객체까지 해제되게 할수 있다.
 그러나 여기엔 약간의 위험이 있다. 만약 m_bAutoDelete 가 TRUE 라면 스레드가
 종료한 후에 객체가 파괴 되므로 그후에 객체를 엑세스 해서는 안된다는 것이다.
 엑세스가 안전한지, 그리고 스레드가 유효한지를 판단하기 위해 객체를 엑세스 할때
 마다 GetExitCodeThread() 가 STILL_ACTIVE 인지 검사하는 것은 아주 멍청한 방법
 이란걸 이미 여러분을 짐작 했을것이다.
 따라서 이런 위험을 피하기 위해서는 CWinThread 객체의 스레드가 종료함을 보고받던

 혹은 생성시에 개체포인터를 ASSERT_VALID() 를 이용해 온전한지 검사해 보는 방법이
다.
 어쨋든 이런 일이 일어나는것은 버그이기 때문에 이렇게 되지 않도록 스스로 안전한
 방법을 강구해야 한다. m_bAutoDelete 를 FALSE로 하고 객체를 직접 delete 하는것도
 방법이 될것이다.

 만약 UI 스레드를 쓴다면 더 간단하다. 여러분은 스레드의 메시큐에 WM_QUIT 메세지
가 
 담기도록 하면 그만이다. PostQuitMessage()  그것으로 그 일을 할수 있다는 것은
 이미 잘 알것이다. 그러나 언제까지나 Post 라는 점을 명심하라. Post 는 메세지를
 부치고 바로 리턴되지 그 메세지가 처리될때까지 기다리지 않으므로 지금 
PostQuitMessage()
 를 했다고 해서 바로 다음 스레드가 종료했다고 생각하면 오산이다. 그렇지 않는가? 
 큐에 메세지를 담았을뿐 아직 스레드는 PostQuitMessage() 다음줄을 실행하고 있을 
뿐이지
 메세지를 디스패치 한적 없다. 만약 스레드 밖에서 UI 스레드를 종료시키고자 한다면
 약간 복잡하다. 적어도 지금까지 아는 방법으로는 WM_QUIT을 유도하는 방법 뿐이다.
 그것은 PostThreadMessage() 로 메세지를 보내서 그 메세지 핸들러가 
PostQuitMessage()
 를 호출하는 것이다. 그리고 PumpMessage() 가 GetMessage()로 WM_QUIT을 읽어 FALSE

 리턴하도록 하고 ExitInstance() 를 수행함과 동시에 Run()을 리턴시키 도록 하는 것
이다.
 이는 WM_DESTROY를 처리하는 윈도우프로시저의 수행방법과 거의 일치한다고 볼수있
다.
 이 방법은 적어도 Release 모드에서는 잘 동작한다. PumpMessage()의 소스코드를 찾
아보면
 알겠지만 디버그 모드에선 검사가 좀 많다. 나는 경험적으로 이러한 시도가 100% 안
전하다고
 말할수 없다는 것을 알린다. 단지 경험적인 방법일뿐 정설인지 아닌지는 나도 모른
다.
 그것은 디버그 모드에서 WM_QUIT 를 받았음에도 불구하고 PumpMessage() 에서 의도하
지 않은
 작업을 시도하여 ASSERT()에 걸리는 것을 수차례 목격했기 때문이다.
 이에대한 정확한 해답을 아직 밝히지 못했다. 어쨋든 이러한 종료작업도 마찬가지로
 PostThreadMessage() 한 놈과 PostQuitMessage()할 놈이 서로 스레드이고 이들둘은 
동시에
 실행되고 있다고 할수 있지만, PostThreadMessage() 를 호출한후 바로 종료를 기대하

 스레드가 WM_QUIT을 처리할수 있다는 것은 절대 있을수 없는 상상이다. 기대도 하지
마라.
 그런일은 절대 없다. 그래서 PostThreadMessage()로 스레드 종료를 기대 했다면 스레
드가
 온전히 종료했는지를 기다려야 하며 그것은 누차 설명하지만 WaitFor..Object() 와 
 GetExitCodeThread() 로 확인할수 있다. 그 이후가 되어서야 비로소 스레드가 종료했
다고
 인정할수 있는 것이다. 언제나 그렇지만 스레드가 휴지상태로 들어가야 하는 순간이
라면
 Sleep(0) 로 같은 우선순위의 스레드에게 CPU 사용권을 양보할 필요가 있고 이는 아
주 
 유용하다.

 
  
 자 이제 그렇게 부드러운 종료가 일어나지 않는다고 가정할때..
 부득이 스레드를 강제로 종료해야할때 그 시나리오는 다음과 같다.

 일단 여러분은 스레드핸들을 WaitFor..Object()에 넣어서 스레드가 종료하도록
 기다리게 할수 있다. 그러나 무한정 기다릴수 없기에 타임아웃을 걸것이다.
 그리고 타임아웃이 지난후 GetExitCodeThread() 로 스레드가 여전히 STILL_ACTIVE인

 체크할수 있다. 만약 STILL_ACTIVE 라면 스레드는 아직도 종료할 생각을 안하고
 혼자서 열심히 돌고있다. 이제 이놈을 죽여야 겠다.
 극약처방인 TerminateThread()로.
 그러나 단지 죽이는 것일뿐 사실 여러분이 얻을수 있는것은 아무것도 없다.
 왜?
 그것은 스레드를 강제로 종료하면 치뤄야 되는 댓가가 있기 때문이다.
 일단 강제종료는 스택을 안전하고 깨끗하게 파괴하지 못한다. 지금 상태 에서 그냥
 실행만 종료될 뿐이고 스택과 사용된 리소스는 해제된다는 보장이 없다.
 뭐 달리 해제할 방법도 없다(아직 나는 모른다). 그냥 프로세스를 종료하고 OS가
 알아서 잘 재활용하기를 기대할 뿐이다. 
 또 그것 뿐이냐. 스레드가 사용한 크리티컬 섹션은 파괴되지 않게 되므로 그 섹션에

 뭔가를 기다리는 모든 다른 스레드들은 교착상태가 될것이다. 끔찍하다!
 나름데로 해결책이라면 그 크리티컬 섹션과 연관된 모든 스레드도 같이 종료시켜야 
 할것이고, 최악의 순간엔 프로세스를 종료해야 할지도 모른다.
 또 있다. MSDN 왈 TerminateThread() 는 커널을 흔들어 버린다. 지고지순 해야할 커
널이
 불안해 진다니... 말할것도 없다. 시스템은 언제 어디서 어떻게 숨을 거둘지 모른다.
 이렇게 강제종료는 우리에게 도움 주는게 없다. 해만 입힌다. 
 만약 꼭 죽어도 강제종료를 해야 겠다면 그냥 프로세스를 종료하게 하는 것을 권한
다.
 적어도 우리 손으로 종료시키는것 보다 OS가 온전하게 종료할수 있게끔 기회를 주라

 것이다. 실제로 프로그램은 종료하지만 이 방법은 시스템을 보다 안정적으로 유지할

 있게 할지도 모른다. 특히 여러분이 서버를 만든다면... 컴퓨터 뻗게 하느니 얌전하

 프로그램만 종료되는게 얼마나 신사적인가.



 *멀티 스레드

 이제 멀티스레드로 넘어오자. 흔히 멀티 스레드를 쓸때 우리는 두가지 모델을 많이 
구현
 한다. 서로다른 작업들을 하는 여러개의 스레드를 동시에 돌리거나, 같은 작업을
 하는 여러개의 스레드를 동시에 돌리는 일. 둘다 멀티스레드다.
 전자쪽은 주로 알고리즘상으로 효율을 극대화할때 많이 쓰이고 후자쪽은 특정 작업을
 병렬적으로 수행해야 할때 많이 쓰인다. 
 둘다 멀티스레드를 받쳐줄수 있는 하드웨어의 지원이 필요하다. 즉 JOB을 처리할 CPU

 개수가 많아야 한다는 점이다. 물론 WIN32 OS 는 선점형 멀티스레딩을 통해 우선순위
데로
 스레드를 스위칭 하기는 하지만 어디까지나 논리적으로 그렇다는 거고 물리적인 뒷받
침이
 없다면 멀티스레드는 퍼포먼스를 충분히 발휘할수 없다. (못쓸 상황이란 뜻은 아니
다!)
 예를들어 CPU하나에서 1에서 10000까지 합을 구하는데 스레드 2개로 나눠서 하면 1개
로 
 할때보다 속도가 빨라질까? 

 택도없는 소리.. 
 더 느리다. 오히려 스레드간의 스케줄링 때문에 더 느리다. CPU만 더 괴로울 뿐이다.
 그러나 멀티스레드를 이용해서 보다 나은 알고리즘적 성능향상을 꾀할수 있는 상황이
라면
 분명 도움이 된다. 또 CPU 를 늘리면 멀티스레딩은 그제서야 제빛을 낼수 있을 것이
다.

 멀티스레드가 되면 일단 골치가 아파지기 시작하는것이 자원에 대한 동기화다.
 즉 전역변수 a가 있을때 여러개의 스레드가 동시에 a 값을 바꾸려고 하면 엉망진창이
 되겠지. 예를 들자면..

 int g_a;

 Thread()
 { 
   g_a = GetA();
   g_a++;
   printf( "%d", g_a);
 }

 Thread() 를 여러개 돌리면 모든 경우에 있어서 g_a 가 얼마가 될지 알수 없다.
 5개의 스레드가 돌면서 그중 3번 스레드가 GetA()로 받은 값이 9일때 3번 스레드는
 10을 출력하기를 기대하겠지만 그 값은 10이될지,11이 될지,12가 될지.. 알수 없다는
것이다.
 그래서 멀티스레드는 동기화가 필요한 것이다.

 동기화 객체의 사용에 대해서는 책에 많이 나오니깐 설명은 생략한다.
 
 단지 동기화 객체를 어떻게 사용해야 성능향상을 꾀할수 있는가가 중요한 문제다.
 멀티스레드 동기화를 쓰다보면 우리는 아래와 같은 문제에 종종 접하게 된다.

 MyObject obj[100];

 Thread()
 {
     ....
     criticalsection.Lock();

     obj[n].DoSomething();

     criticalsection.Unlock();
     ....
 }

 Thread() 가 10개 실행되고 있다고 했을때..
 우리는 obj 가 뒤죽박죽이 되는걸 막기위해 크리티컬 섹션으로 obj 접근을 보호했다.
 따라서 10개중 크리티컬 섹션에 진입한 하나의 스레드만이 obj에 접근하도록 했다.
 이제 obj 는 여러개의 스레드로 부터 안전하며 동기를 유지할수 있게 되었다.

 그러면 그것으로 끝났는가?

 여기에는 치명적인 성능결함이 있다. 즉 obj 의 DoSomething() 이 한번에 하나밖에
 호출되지 않는다는 점이다.

 당연한거 아니냐고?

 만약 우리가 CPU 10개를 가지고 있다고 치자. 10개의 스레드는 단순한 이론적으로
 모두 동시에 실행되고 있다. 그러나 정작 DoSomething() 은 동시에 실행되지 못한다.
 CPU가 10개든 100개든 마찬가지다. 
 만약 obj[n] 의 n이 같은 번호를 가진 스레드가 여러개라면 그것들 끼리는 철저히
 동기가 되어 한번에 하나만 DoSomething()이 호출되어야 하지만, n이 다른 것들은?
 가령 Thread() 1,5,6 번은 n=4 이고 Thread() 2번은 n=1이고 Thread()3번은 n=5라고
 했을때, Thread() 2번과 Thread()3번은 Thread() 1,5,6 번과 전혀 상관이 없는
 객체를 엑세스 하고 있기 때문에 동시에 실행되어도 상관없다.
 즉 1,5,6번 스레드만 동기화 되면 되는데 괜히 2번과 3번도 실행되지 못하는 사태가
 벌어지고 있는것이다.
 이게 무슨 멀티스레드냐. 
 차라리 스레드 안쓰고 while 로 10번 돌지.
 괜히 스레드 끼리 컨텍스트 스위칭 한다고 CPU만 축내고 속도만 느려진다.

 그/래/서!

 
 *코드를 동기화 하지 말고 자원을 동기화 하라.
 
 위의 예와 같이 멀티스레드에서 코드를 동기화 하면 모든 스레드가 코드 진입을
 못하기 때문에 (적어도 객체가 여러개일때)특정 객체가 동기화 되어야 한다면 
 객체가 각각 동기화 되어야 한다는 것이다.

 이 예를 위 예제를 수정하여 보도록 하자.

 class obj
 {
     public:
        DoSomething();
        criticalsection_type m_cs;        <-- 이것.
 };
 
 Thread()
 {
     ....
     obj[n].m_cs.Lock();

     obj[n].DoSomething();

     obj[n].m_cs.Unlock();
     ....
 }

 위 예제에서 obj 클래스는 크리티컬섹션 객체를 멤버로 가지고 있다.
 그리고 Thread() 에서는 각 객체가 가진 크리티컬섹션을 이용해 객체를 보호하고 있
다.
 이렇게 함으로서 앞에서 언급한 서로다른 객체로의 접근에서도 모든 스레드가 동시에
 실행되지 못하는 문제를 해결할수 있다.
 그리고 정확히 같은 객체를 엑세스 하는 스레드 끼리만 동기화 되고 아닌경우는
 모두 동시에 실행될수 있게 되었다.
 
 이제서야 멀티스레드를 쓰는 이점을 제대로 살린 셈이다.


 * 또다른 의문.
  
 우리는 이런 의문에 빠질지도 모른다. C런타임 함수들은 과연 스레드로 부터
 안전하게 엑세스 되는가? 
 토큰을 추출하는 strtok() 함수는 내부적으로 전역변수를 사용한다.
 그러면 멀티스레드에서 내부적으로 전역변수를 사용하는 C런타임 함수들을
 사용하기 위해 모두 크리티컬섹션으로 보호해야 하는가?
 음.. 보호해야 정상이긴 하다.
 그러나 그렇게 까지 하기엔 코딩이 너무 가혹하다. 
 그래서 VC++ 은 C런타임 함수의 버전을 2가지로 관리한다고 한다.
 첫번째가 싱글스레드 용이요, 둘째가 멀티스레드용. 
 아주 앞쪽에서 이야기 했지만 우리는 project->setting->C/C++ 의 Code generation
 탭에서 멀티스레드 사용의 옵션을 선택할수 있다.
 그 옵션이 선택되면 빌드타임에서 링커가 알아서 멀티스레드용 C런타임 함수를
 링크한다. 따라서 결론은 그런 걱정은 붙들어 메시라는 거다.
 멀티스레드용 C런타임 함수들은 스레드 스택에 그 전역변수를 저장하게 함으로써
 멀터스레드에서도 변수가 공유되지 않도록 보장한다.
 참으로 다행스러운 일이다.


 * 스택의 위험.

 지금까지는 그냥 지나왔지만 스레드를 생성하는 함수는 꼭 스택사이즈를 파라미터로
 같고 있다. 대게의 경우 0을 넣는데 그러면 디폴트 1M 크기의 스레드 스택이 생긴다.
 그러나 디폴트가 1M 인것은 컴파일러 옵션에서 1M를 잡았기 때문이다.
 그 디폴트 크기를 변경하고 싶을때는 project setting 의 Link 에 stack 
allocations 에서
 변경할수 있다. 그러나 변경하지 않는게 좋을듯 하다. 스택에 대한 관리는 95계열의 
OS와
 NT 계열의 OS가 약간 틀린데 COMMIT 하는 스택의 양도 틀리고 위치도 틀리다.
 대게 건드리지 않는게 수명연장에 도움이 될것 같다. 스택에 집어넣을게 많거든 
 그냥 Heap 에 넣어라.

 문제는 스택오버플로우나 스택언더플로우를 조심해야 한다는것이다.
 NT 든 95시리즈든 스택오버플로우나 언더플로우를 막기위해 나름데로의 디자인을 가
지고
 있다. 그리고 만약 주어진 스택의 경계를 넘어설때는 예외가 발생하고 또 심한 경우
에는
 엑세스 위반이 일어난다. 문제는 우리가 스택의 경계를 넘지 않도록 스스로 주의해
야 한다는
 점이다. 스택의 크기가 1M 라면 Intel CPU의 경우 페이지의 크기는 4k. 따라서 정확

 256개의 메모리페이지가 사용된다. NT의 경우 그중 페이지2개는 스택의 상위와 하위

 표현하기 위해 사용되고 95시리즈의 경우 아래위로 64k 씩 오버/언더 체크 페이지가 

 할당된다. 뭐 골치아픈 원리로 인해 스택에서 메모리사용이 증가되어 오버/언더를 칠

 앞서 말한것 처럼 예외나 에러가 나기는 하지만 진짜 무서운 것은 에러가 나지 않고
 소리소문 없이 프로세스를 종료시키게 될수 있다는 것이다. 또 스택의 오버/언더를 
체크
 하는 범위를 훌쩍 뛰어넘어(NT는 양끝의 4k페이지, 95시리즈는 양끝64k페이지) 엑세
스 
 하려고 하면 결과는 뻔하다. OS 는 멍청하게 있을것이고 엑세스된 메모리는 보기좋게
 손상된다. 따라서 스택의 한계에 점점 다다르면서 엑세스가 될때는 예외나 에러로 끝
나지만
 그렇지 않고 훌쩍 뛰어넘어 엑세스 하는 코드를 만들면 그야말로 원인도 모르는
 버그를 만들기 십상이라는 거다. 
 결론은 하나밖에 없다.
 스택에서 스스로 조심하라는 거다. 1M  넘지 않도록. 또 포인터 조심하고..
 


 후...  더 적고 싶은게 있는데 시시콜콜 한것 같다.
 나머지 부분은 왠만한 책에 다 나올것 같다. 뭐 사실 위의 내용도 찾아보면 다 나오
지만.
 Jeff Prosise 의 MFC Windows95 programming 에서는 워커스레드에서 MFC 객체 사용에
 대한 주의점을 언급하고 있으니 참고바란다. 또 Jeffrey Richter 의 Advanced 
Windows NT
 에서는 스레드에 대한 보다 자세하고 포괄적인 내용인 많이 있으니 꼭 읽어보길 권한
다.
 뭐 내가 가진 책은 초판이라 요즘 나오는 5판? 에는 더 좋은 내용이 있으리라 본다.
 프로젝트 끝나고 나태하게 있다가 갑자기 무리를 했더니 삭신이 쑤신다.. -_-;
 자야지.. 모두 잡시다!

 ^_^;


--------------------------------------------------------------------------------
--

 프로그래머 와 크래커. 
 한쪽은 텅빈 에디터에서 유용한 제품을 만들어내는 창조자.
 또 한쪽은 창조된 것을 변형하고 허점을 이용해 파괴하는자.
 크래커의 수준이라면 이미 왠만한 OS아키텍처나 프로그래밍
 은 실력이 상당한 경우가 많다. 만든사람 이상은 알아야 그걸
 고치지 않겠는가.
 이바닥엔 그런 크래커를 존경의 대상으로 보는이도 있고, 저주
 하는 이들도 있다. 
 그렇다.
 그들도 노련한 실력자이며 그전에 존경 혹은 저주를 받을수 있는
 한사람의 인격체이다. 인간이기에 선택할 권리가 있다.
 그러나 그들은 상당한 실력을 쌓은뒤에 무엇을 선택하였는가?
 창조하는이가 피와 땀을 댓가로 지불한 결과물을 파괴하고, 
 때로는 그들을 비웃기도 하며, 마음에 들지 않으면 공격하는
 그런 일들을 스스로 선택한 것이다.
 악마가 되기는 쉬워도 천사가 되기는 어렵다 했던가.
 상당한 지식과 기술을 익힌뒤에 오는 크래킹의 달콤한 유혹.
 그들에게는 창조하는이 이상의 기술적 경외가 뒤따르는
 크래킹의 세계앞에서 그 문을 열어버린 것이다.

 그러나 묻고 싶다.
 진정 크래킹이 스스로 높은 경지라고 생각한다면,
 어찌 그 유혹을 뿌리치고 더 높은 경지의 창조자를 선택할수는
 없었는지를..
 크래킹이 있어야 더욱 보안 기술의 발전이 온다는 생각이
 그저 노력에 노력만을 거듭한 많은 일꾼들의 생계수단인
 직장을 고사시킬수 있다는 것을..

 단지 우리는 선택에 놓였을 뿐만 아니라 선택으로 타인의 생계를
 쥐고 있다. 크래커든 프로그래머든 둘다 시작은 해커를 꿈꾼
 젊은이 였으며, 인격을 가진 사람이다.
 악이 없이 선이 없듯이 크래커가 영원히 존재하지 않을수는 없을지라도
 지금 당신의 열정과 땀으로 주어질 선택이 진정한 존경으로 돌아올수
 있도록 유혹을 이겨낸 진짜 승자가 되어야 하지 않을까......


-태권브이-

 

 

 

 

링크 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=1797

반응형