정성훈
    Thead 죽이기
  http://multimedia.tistory.com/?page=2



1. Process와 Thread

쓰레드라는 놈을 이해하기 위해선 프로세스란 놈을 먼저 이해해야한다. 프로세스는 EXE란 확장명을 가지고 파일을 실행시켰을 때 실질적으로 컴퓨터의 메모리에 자리잡고 앉아서 실행되는 형태를 말한다. 예를 들면 프로그램을 마우스로 더블클릭하면 OS가 그 프로그램을 실행하기 위해, 알맞게 메모리상에 그 프로세스가 실행가능한 영역을 잡아주고(이런 영역에는 여러가지 영역이 있다.코드부 or 스택부), 프로그램은 거기에 들어앉아서 온갖 사용자의 명령을 다 받으며 자기의 일을 수행한다.

즉, 프로세스는 메모리에 적재된 상태로 동작하는 프로그램이다.


프로세스를 이루는 기본단위가 쓰레드라 볼 수 있다. 프로세스는 적게는 수십개, 많게는 수백개의 Thread로 이루어져 있다. 즉 이런 쓰레드들이 실처럼 일률적인 순서대로 늘어져 원하는 작업을 수행하고 있는 것이죠. 쉽게 보면 쓰레드 하나가 특정한 작업을 하나 수행한다고 생각 할 수 있다.


프로그램을 짜다보면 여러가지 상황이 발생하는데, 블록킹모드, 논블록킹 모드, 동기모드, 비동기모드 등등..

예를 하나 들어 보면,

while( true )

{

     int count;

     printf(  “Current number %d”, count );

     count++;

}

위의 루틴에 들어가게 되면 프로그램은 블록(무한 Loop에 빠짐)됩니다. 사용자는 어떤 입력도 프로그램에 가 할 수 없습니다. 그래서 사람들은 저런 반복적으로 작업하는 계산 루틴 등을 쓰레드로 만들어 버린다. 즉 계산과정을 쓰레드로 만들어서 하나의 작은 작업으로 처리해 버리는 것이다. 그러므로 저렇게 반복적인 작업이나 시간이 오래 걸리는 작업, 또는 다른 작업과 병행해서 일을 해야 하는 경우는 쓰레드를 사용하게 된다.


물론 처리 속도 문제는 조금 다른 문제이다. 단지 쓰레드로 프로그램을 작성하면, OS차원에서 쓰레드가 CPU를 사용할 시간을 분배(Schedule)하는 것뿐이다. 단지 이 시간의 간격이 무지 짧기 때문에 사용자는 느끼지 못하고, 마치 순간적으로 동시에 작동하는 것처럼 보인다.


쓰레드는 사용자에게 융통성 있는 사용환경을 만들어 주기 위해서 개발자가 충분히 검토하고 개발에 적용해야할 테크닉이다.


2. 쓰레드의 장점

A. 애플리케이션의 일부가 비동기적이고 병렬적으로 실행가능하게 함

B. 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에게 동일한 서비스 제공가능

C. 블록(죽어버릴 수 있는)될 가능성이 있는 작업을 수행할 때 방지할 수 있음

D. 멀티프로세서 시스템을 효과적으로 사용 할 수 있음


A. 애플리케이션의 일부가 비동기적이고 병렬적으로 실행가능하게 함

동기적이란 어떤 작업이 순차적으로 실행되어야 함을 말한다. 예를 들어 어떤 함수 두개가 있다고 가장하자.


int a = 2, b = 3, c;

int d = 4, e = 5, f;

funcA { c = a+b;}

funcB { f = d+e;}

단일 쓰레드 환경에서는 함수A를 호출한 다음에 함수B를 호출한다.

funcA()

funcB()

그런데 함수A와 함수B가 서로 완전히 다른 데이터를 가지고 작업을 한다면, 함수가 실행되는 순서에는 상관이 없다. 즉 함수의 호출 순서가 바뀌어도 상관이 없다.

funcB()

funcA()


그러나 새로운 함수 C를 만들어 보자.

in funcC { return c + f;}

위의 함수는 함수A와 함수B의 결과값을 가지고 연산을 하기 때문에, 함수A와 함수B가 완전히 실행이 끝난 다음에야 호출할 수가 있다. 위와 같이 순차적으로 실행을 해야만 하는 순차적인 프로그램 방식을 동기화(Synchronization)라고 합니다. 비동기화는 위와 같이 순차적으로 실행을 안 해도 되는 것이다.


단일 쓰레드를 지원하는 OS에서는 반드시 위와 같은 절차를 가지고 프로그래밍을 해야 하지만 멀티 쓰레드를 사용하는 OS에서는 프로그램을 작성할 때 각 부분 사이에 상호관계를 반영하여 설계를 할 수 있도록 하는 것이다.


B. 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에게 동일한 서비스 제공가능

Network 프로그램 같은 경우 사용자의 접속을 계속 처리하기 위해서 쓰레드를 사용합니다. 즉 사용자가 접속 요청을 하면 하나의 쓰레드와 연결시켜준다. 여러 사용자가 단일 쓰레드의 서버 프로그램에 접속해서 서비스를 받는다고 가정하면, 한 명이 접속해서 그 사람이 접속을 끊을 때까지 다른 사용자는 서비스를 받을 수 없다.


C. 블록(죽어버릴 수 있는)될 가능성이 있는 작업을 수행할 때 방지할 수 있음

Network 프로그래밍에서 블록 되는 코드가 있다고 가정하자. 대표적으로 보면 accept()가 있는데, 이 함수는 사용자가 접속요청을 할 때까지 멈춰 있는다. 사용자 접속요청이 없으면 무한정 기다리게 된다. 그래서 접속요청을 받는 부분을 쓰레드로 만들어서 프로그램이 블록되지 않게 하는 것이다.

또 다른 예로, 탐색기에서 덩치가 큰 파일을 복사를 걸어놓고도 곧바로 탐색기를 다시 있는데, 이는 복사하는 루틴을 쓰레드로 만들었기 때문이다.


D. 멀티 프로세서 시스템을 효과적으로 사용 할 수 있음

CPU가 많은 시스템에서 쓰레드를 돌리면 좀더 효율적이다.


3. 멀티쓰레드 애플리케이션 구조

멀티쓰레드를 이용한 애플리케이션을 작성하는 구조에는 3가지 방법이 있다.

A. boss/worker모델.

B. work crew모델.

C. 파이프라이닝 모델.


A. Boss/Worker Model

쓰레드(주쓰레드)가 필요에 따라 작업자 쓰레드를 만들어 내는 경우에, 위에서 예를 든 것과 같이 접속받는 부분을 주쓰레드(boss)로 돌리고, 접속요청이 오면 새로운 쓰레드(worker)를 만들어 사용자와 연결시켜 주는 방법입니다.


B. Work Crew Model

어떤 한 작업을 여러 개의 쓰레드가 나눠서 하는 방식이다. 예를 들어, 집을 청소하는 작업에 대한 쓰레드를 여러 개 돌려, 방바닥 닦는 사람, 먼지 터는 사람 이런 식이다. 특정한 청소를 하는 작업이 쓰레드 하나가 되는 것이다.


C. Pipe Lining Model

이 구조에서는 순서대로 실행되어야할 작업이 여러 개가 있을 경우, 작업 1은 작업 2에게 전달하고, 작업2는 작업3에게 전달하는 방식으로 순차적으로 진행되어야 하지만 최종결과를 여러 개 만들어내기 위해서 동시에 수행될 필요가 있을 경우 멀티쓰레딩을 사용한다. 예를 들면 공장의 라인에서는 서로 다른 작업들을 수행하지만, 결국 하나의 결과물을 만들어 내기 위한 과정이다.


4. 쓰레드의 생성

쓰레드는 UI( User Interface ) Thread와 Worker( 작업자 ) 쓰레드로 나뉜다.

UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는 쓰레드를 말한다.) 쓰레드이다.

Worker 쓰레드는 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수를 위한 것이다.


윈도우에서 쓰레드를 생성하는 방법은 여러가지가 있다. 즉 윈도우라는 OS에서 제공해주는 라이브러리함수를 가지고 쓰레드를 만드는 것입니다.


Wokrer 스레드를 만드는 방법을 알아보자.

어떤 프로그램이 Dialog 기반으로 돌아가며, 화면에 2초에 한번씩 숫자를 더하면 화면에 바뀐 숫자를 표시한다고 가정한다.


① 화면에 무한적으로 숫자를 뿌리는 함수를 만듦

UINT ThreadFunc( void* pParam )

{

   int count;

   while(true)

{

   count++;

   GetDlgItem( IDC_STATIC )‐>SetWindowText( atoi(count) );

   Sleep( 2000 );

}

}

//정확한 코드는 아님

// IDC_STATIC는 static_control 의 resource id 임


이 함수는 2초에 한번씩 count를 증가시키면서 Dialog box에 증가된 값을 화면에 뿌린다. 이 함수는 쓰레드로 돌리려 한다.


② Worker 쓰레드 생성하기

작업자 쓰레드로 특정한 작업을 하는 사용자 정의 함수를 만들기 위해서, 윈도우에서는 여러가지 쓰레드 생성 함수를 제공해 준다.

1.  CreateThread()

2.  _beginthread(), _beginthreadex()

3.  AfxBeginThread(), AfxBeginThreadEx()


1. CreateThread()

win32함수로써, 중요한 parameter만 살펴본다.

     HANDLE handle;

     Handle = CreateThread( Threadfunc(), Param );

첫번째 parameter는 위에서와 같이 사용자가 쓰레드로 돌려야할 작업함수를 써 주며, 두 번째 parameter는 작업함수에 parameter값으로 전달할 값이다. 이 parameter는 VOID*으로 되어 있기 때문에 4BYTE이내의 값은 어떤 값이든 들어갈수 있다. 대신 TYPE CASTING을 해주어야 합니다.(ex. (int) 3). 이 함수가 올바르게 실행이 되면 쓰레드에 대한 핸들을 반환하는데, 이 핸들을 가지고 쓰레드를 조작할 수 있게 된다.

대표적으로 쓰레드를 닫을 때 CloseHandle()함수를 사용해서 쓰레드 핸들을 넣어주고 쓰레드를 닫아 준다. 그러나 이 함수로 생성된 쓰레드를 닫을때는 ExitThread()면 된다.


2. _beginthread(), _beginthreadex()

CreateThread는 쓰레드에서 win32 API함수만 호출할 수 있다. 즉, 사용자가 어떤 작업을 하는 함수를 만들 때 CreateThread로 만들게 되면, 그 함수안에서는 win32 API만 사용할 수 있고, C함수나 MFC는 못쓴다.

그러나 _beginthread() 함수는 win32 API와  C 런타임 함수를 사용할 때 사용한다.

주의할 점은 이 함수도 쓰레드 핸들을 리턴하는데, 이 핸들을 가지고 쓰레드에 조작을 가 할 수 있다. 대신 이 함수를 사용하면 C 런타임 라이브러리가 핸들을 자동으로 닫으므로 이를 직접 닫을 필요는 없다. 하지만, _beginthreadex는 스레드 핸들을 직접 닫아야 합니다. 그리고 이 쓰레드를 닫을 때는 _endthread(), _endthreadex()를 사용하면 된다.


3. AfxBeginThread(), AfxBeginThreadEx()

실질적으로 가장 자주 사용하는 쓰레드 생성함수 이다. 이 함수를 이용하면 사용자 정의 함수내에서 MFC, win32 API, C 런타임 라이브러리등 여러가지 라이브러리 함수들을 전부 사용할 수 있다. 이 함수는 리턴값이 CwinThread* 형을 리턴하는데, 이 객체를 사용하면, 생성된 쓰레드에 여러가지 조작을 가할 수 있다. AfxEndThread()를 사용해서 종료 시킬 수 있다. 쓰레드가 종료되면 MFC는 쓰레드 핸들을 닫고 리턴값으로 받은 CwinThread*객체를 제거합니다.


5. MFC에서 작업자 쓰레드 생성하기..

MFC에서 쓰레드를 만드는 방법은 두 가지가 있다. CwinThread의 멤버 함수인 CreateThread를 사용하는 방법과 AfxBeginThread()를 사용하는 방법이다. 후자는 CwinThread객체를 만들 수 있다.


또한 MFC에서는 AfxBeginThread의 서로 다른 버전 두개를 정의 하고 있다. 하나는 작업자 쓰레드를 위한 것이고, 하나는 UI쓰레드를 위한 것인데, 이 두 가지 정의대한 소스코드는 Thrdcore.cpp에서 볼 수 있다.


CwinThread* pThread = AfxBeginThread( Threadfunc, &threadinfo );


▪ nProiority는 쓰레드의 우선순위를 지정한다. 기본적으로 THREAD_PRIORITY_NORMAL이 들어가는데 이는 평범한 순서로 작동시키겠다는 말이다. 이 우선순위는 나중에 리턴값으로 받은 CwinThread* 의 멤버 함수인 SetThreadPriority를 가지고 변경해 줄 수 있다.

▪ dwCreateFlags는 0 아니면 CREATE_SUSPEND이다. 기본값은 0이 들어가는데 0이면 바로 쓰레드가 실행되는 것이고 두 번째이면 쓰레드는 정지된 상태로 만들어지고 리턴값으로 받은 객체의 멤버함수인 ResumeThread를 호출하면 쓰레드를 시작한다.

▪  lpSecurityAttrs는 보안속성이다.


AfxBeginThread() 함수의 첫번째 인자로 들어갈 작업함수의 원형은 다음과 같다.

     

     UINT ThreadFunc ( LPVOID pParam )


이 함수는 static 클래스 멤버 함수 이거나 클래스 외부에서 선언한 함수여야 한다.

즉, 어떤 함수를 클래스 멤버함수로 선언해서 쓰레드로 사용하려면 클래스내부에 함수 프로토타입 선언시 static로 선언해 주어야 한다는 건데, static 멤버 함수는 static 멤버 변수에만 접근 할 수 있기 때문에 제약이 많다.


인자값은 AfxBeginThread에서 두번째 인자로 들어갔던 32bit값의 파라미터 인데, 일반적으로 그냥 데이터형을 넘겨줄 수도 있고, 많은 데이터가 있을 때에는 구조체를 만들어서 포인터로 넘겨주어서 함수내부에서 다시 풀어서 사용한다.


쓰레드 함수 내부에서 만들어진 객체나 변수들은 쓰레드 자신의 내부 스택에 생성되기 때문에 다른 쓰레드에서 데이터를 변경하거나 하는 그런 문제들은 없다.


6. 쓰레드 멈추고 다시 실행하기

AfxBeginThread() 함수로 작업을 할 함수를 스레드로 돌리고 그 스레드가 올바르게 작동이 되면 CwinThread*형을 리턴값으로 받는다는 것을 알게 되었다. 이렇게 되면 우리는 이 실행시킨 스레드를 사용자가 멈추고 싶을 때 멈추게 할 수 있고, 또 멈췄으면 언젠가 다시 실행을 시켜야 한다.

우선 실행중인 스레드함수를 멈추게 할때는 우리가 리턴값으로 돌려받은 CwinThread*의 변수를 가지고 작업을 할 수 있다.

     CwinThread::SuspendThread()

위의 함수가 바로 실행중인 스레드를 멈출 수 있게 하는 함수이다. 이 함수는 현재 실행중인 스레드 내부에서 호출할 수도 있고, 또한 다른 쓰레드 내에서도 호출할 수가 있다.

     CwinThread::ResumeThread()

위 함수는 중지된 쓰레드를 깨우는 함수이다. 이 함수는 위의 SuspendThread같이 스레드 자기자신의 내부에서는 호출할 수가 없다. 다른 쓰레드나 메시지 Handler 함수들을 이용해서 ResumeThread()를 호출해야지 멈춰진 쓰레드를 다시 실행 할 수 있다.


(참고) 스레드 내부로 복잡하게 들어가면 내부적으로 각 스레드 함수는 Suspend count란 것을 유지하고 있어서 이 값이 0일 때만 스레드가 CPU시간을 할당받아 실행이 된다. 즉 AfxBeginThread함수에 dwCreateFlags에 CREATE_SUSPEND를 넣어줘서 실행한다거나 CwinThread::SuspendThread()를 실행하거나 하면 Suspend count는 1이 증가가 된다. 그래서 스레드는 멈추게 되고 다시 CPU로부터 시간을 할당받아 스레드가 활동을 재개하게 하려면 ResumeThred()를 호출해서 suspend count를 0으로 만들어 주어 CPU시간을 할당받게 해서 스레드를 다시 시작한다. 내부적으로 이런 메커니즘을 가지고 스레드는 동작하기 때문에 SuspendThread()를 두 번 호출하면 suspend count가 2가 되고, RecumeThread()도 두 번 호출해 줘야 스레드가 다시 활동할 수가 있다.



7. 스레드 잠재우기

쓰레드를 잠재우는 이유는 크게 두 가지가 있다.

하나는 에니메이션을 구현하거나 아날로그 시계의 바늘 같이 시간이 경과함에 따라 화면에 그림을 그린다든지 하는  경우, 즉 시간 경과에 따라 실행하여야 하는 프로그램의 경우이고,

두 번째는 어떤 스레드가 현재 실행되어야 하는 시점에서 다른 쓰레드에게 양보를 할 경우에 사용된다.

첫 번째의 경우는, 한마디로 쓰레드 내부에서만 사용하는 쓰레드 타이머라고 생각할 수 있다. 즉 일정시간이 지나면 반복적으로 작업을 할 때 유용하다.


쓰레드 내부에서도 Sleep()라는 걸 이용하면 쓰레드 내부 루틴을 타이머를 사용했을 때와 비슷하게 만들 수 있다.

쓰레드를 잠재울 때는 다음의 함수를 사용한다.

     ::Sleep( 0 );

위 함수는 win32 API함수이다. 따라서 사용할 때 :: 를 이용해서 호출한다. 만약 위의 함수를

     ::Sleep( 5000 );

하면 이 쓰레드는 5초 동안 CPU시간을 쓰지 않게 되며, 이렇게 잠들어 있을 때는 다른 쓰레드가 CPU시간을 얻어서 활동하게 된다.


두번째는 위의 첫 번째 예와 같이 인자로 0을 주는 경우인데, 이 경우에 현재 스레드를 정지하고(0 이면 0.001초인 이데..멈추다니..) 이 스레드가 CPU시간을 풀어줄 때까지 기다리던 다른 스레드가 실행된다. 그런데 다른 스레드는 이 스레드와 우선순위가 같아야 한다. 그리고 같은 우선순위의 스레드가 없다면 현재 sleep(0)을 실행했던 스레드가 계속 실행이 된다.

스레드 안에서 sleep()를 쓰는 경우는 시간의 지연을 두고 작업을 해야 하는 경우에 많이 사용된다. (ex. 아날로그 시계)


CwinThread* pThread = AfxBeginThread( TimeFunc, pParam );


UINT TimeFunc( void* pParam )

{

     void* param = (void*) pParam;  // 사용자가 넘겨준 데이터를 사용하기 위함

     while( true )

     {

     ::Sleep( 1000 );          // 이곳에서 1초가 멈춰진다.

     DrawTime();             // 초침을 그리는 함수     

     }

}


7. 스레드 죽이기

쓰레드를 죽이는 이유는 자기의 일을 다 마치고 나면 당연히 쓰레드는 없어져야 하기 때문이다. 그렇지 않으면,  쓰레드는 여전히 자기가 들어앉은 메모리 속에 앉아서 컴퓨터 리소스만 잡아먹고 있게 된다.

스레드를 죽이는 방법엔 두 가지가 있다.

     ① 스레드 내부에서 return을 시킬 때.

     ② AfxEndThread를 호출할 때.


안전한 방법은 스레드 내부 자체에서 return문을 이용하는 것이 안전합니다. 탐색기도 이와 같은 방법을 사용합니다. 복사가 다 끝나면 자동적으로 return을 시키기 때문에 스레드가 알아서 죽는다. 이런 방법은 쓰레드가 오랜 시간을 작업하는경우에 사용한다. 즉 언젠가는 결과를 얻어서 끝 날수 있는 일을 할 때 사용하게 되고,  대개 무한루프를 돌면서 하는 일은 AfxEndThread를 사용해서 끝낸다.


위의 첫 번째 방법과 같이 return을 받았을때는 GetExitCode를 이용해서 검색할 수 있는 32bit의 종료 코드를 볼수 있다.


DWORD dwexitcode;

::GetExitCodeThread( pThread‐>m_hThread, &dwExitCode );

// pThread는 CwinThread* 객체의 변수이고..

// m_hThread 는 CwinThread내부의 생성된 쓰레드 핸들


이 문장은 실행이 종료된 스레드의 상태를 보여주는 문장이다. dwExitCode에 스레드가 return 하면서 나온 32bit의 종료값이 들어가게 된다. 만약 실행중인 스레드를 대상으로 저 코드를 쓰게 된다면 dwExitCode에는 STILL_ACTIVE라는 값이 들어가게 된다.

그런데, 위의 코드를 사용함에 있어 제약이 있다. CwinThread*객체는 스레드가 return 되어서 스스로 종료가 되면 CwinThread 객체 자신도 혼자 제거되어 버린다. 따라서 delete시켜주지 않아도 메모리에서 알아서 없어진다.

return이 되어서 이미 죽어버린 스레드를 가지고 pThread‐>m_hThread를 넣어주면, 이것은 이미 return되어 죽어버린 스레드의 핸들을 가리키고,  Access위반이란 error메시지가 나오게 된다.

이런 문제를 해결하려면 CwinThread* 객체를 얻은 다음 이 객체의 멤버 변수인 m_hAutoDelete를 FALSE로 설정하면 스레드가 return을 해도 CwinThread객체는 자동으로 제거 되지 않기 때문에 위의 코드는 정상적을 수행된다.


이런 경우에 CwinThread*가 더 이상 필요가 없어지면 개발자 스스로 CwinThread를 delete시켜 주어야 합니다.  또 다른 방법으로 스레드가 가동이 되면 CwinThread*의 멤버변수인 m_hThread를 다른 곳으로 저장을 해놓고 이 것을 직접GetExitCode()에 전달을 하면 그 쓰레드가 실행중인지 한때는 실행되고 있었지만 죽어버린 스레드인지 확인이 가능하다.


int a = 100;              // 파라미터로 넘겨줄 전역변수.

CwinThread* pThread   // 전역 쓰레드 객체의 포인터 변수.

HANDLE threadhandle;  // 스레드의 핸들을 저장할 핸들변수.


Initinstance() // 프로그램초기화.

{

     // 프로그램 실행과 동시에 스레드 시작.

     1번방법:pThread = AfxBeginThread( func, (int) a );

     // 스레드가 리턴되면 자동으로 CwinThread객체가 자동으로 파괴되지 않게 설정.

     2번방법:pThread‐>m_hAutoDelete = FALSE;

     // 쓰레드 핸드를 저장. 위의 m_hAutoDelete를 설정하지않았을경우..

     threadhandle = pThread‐>m_hThread;

}


MessageFunction()  // 어떤 버튼을 눌러서 스레드의 상태를 알고 싶다..

{

     char* temp;

     DWORD dwExitcode;

     // 스레드 객체의 m_hAutoDelete를 fasle로 설정해서 스레드가 return되어도

     // 객체가 자동으로 파괴되지 않아서 핸들을 참조 할수 있다.

     1번방법:         ::GetExitCode( pThread‐>m_hThread, &dwExitcode);

     // 스레드가 종료되고 미리 저장해둔 핸들을 이용할경우..

     2번방법:         ::GetExitCode(threadhandle, &dwExitcode);

     sprintf( temp, “Error code : %d”, dwExitcode );

     // 스레드 객체 삭제..

     1번방법:         delete pThread;

     AfxMessageBox( temp );

}


func( void* pParam )

{

     int b = (int) pParam;

     for( int I = 0; I < b; I++)

     {

     // 어떤일을 한다. 

     }

return;  // 작업이 끝나면 리턴한다. 이때 스레드 자동으로 종료.

}


위의 코드는 어떠한 작업을 수행하는 쓰레드를 돌리고 작업이 끝나면 자연적으로 리턴을 시키는 가상 코드이다.


1번째 방법은 스레드를 생성하고 m_hAutoDelete를 false로 해서 스레드가 return해서 자동종료해도 CwinThread를 자동파괴하지 않게 하고, GetExitCodeThread()를 호출한다. 밑에서 delete 는 꼭 해야한다.


2번째는 m_hThread를 다른 핸들변수에 저장해 놓고..스레드가 return되면 CwinThread*도 같이 파괴가 된다. 원래 저장한 핸들을 가지고 GetExitcodeThread()를 호출해서 한때 존재했지만 종료된 쓰레드를 검사하는 것이다.


7. 스레드 조작

◎ 하나의 스레드에서 다른 스레드 종료시키기..


① ================================================================================

두개의 스레드가 동작하고 있을 때 하나의 스레드에서 다른 스레드를 일방적으로 종료시키는 방법을 알아본다.

*** 스레드 A ***

static BOOL bContinue = TRUE;

CwinThread* pThread = AfxBeginThread( func(), &bContinue ) 

//.........

bContinue = FALSE;


*** 스레드 B ****

UINT func( LPVOID pParam )

{

     BOOL* pContinue = (BOOL*)pParam; //스레드 A에서 넘겨준 종료플래그

     While( *pContinue )

     {

     //..........

     }

     return 0;          // 끝나면 스레드 B 자동종료.

}


일반적으로 스레드 사이에서 서로를 사살할 때 많이 쓰는 방법입니다. 이 방법은 보통 BOOL형식의 플래그를 하나 둠으로써 그 값을 어떤 스레드에서 변형하여 다른 스레드에서 그 값의 변화를 인지하게 함으로써 스레드를 자동종료( return )시켜버리는 것이다. 보통 이것은 스레드들이 통신하기에 적합하지 않은 방식이라고 많은 서적에서 설명하고 있다. 보통은 스레드들의 동기화 객체를 이용하는 적이 효과적인 방법이다.


우선은 스레드 A에서 static 형의 정적인 BOOL형 변수를 하나 만든다. Static으로 선언된 변수는 프로그램이 생성됨과 동시에 자동으로 메모리에 올라가 있게 된다. 그래서 그 변수가 선언된 블록을 빠져 나오게 되더라도 계속적으로 그 값을 유지하고 있다. 그래서 static이라고 플래그 변수를 선언하고 TRUE로 초기화 시켜 둔다.


그러면 스레드 B는 BOOL형 값의 플래그를 판단하여 실행여부를 결정하는 while루프안에다가 스레드에서 수행해야 할 일을 넣어주고, 스레드 A에서 더 이상 B가 존재할 이유가 없다고 생각하면 플래그의 값을 FALSE 로 변경시켜서 스레드 B를 자연스럽게 죽게한다.

그런데 이 방법은 대부분 사용되지 않는다.(비효율적이므로) 왜냐하면, 스레드 A에서 bContinue = FASLE로 설정한 다음 어떤 작업을 다시 들어가는데, 만약 CPU에서 스레드 스케줄링을 잘못하여 스레드 B가 종료되기 전에 스레드 A에서 또 다른 어떤 작업을 들어가야 한다면, 원하지 않았던 결과를 초래한다. 


② ================================================================================

그래서 우리는 이때 스레드 A에서 스레드 B가 죽을 때까지 기다리는 매커니즘(방법)이 필요하다.


// 스레드 A

static BOOL bContinue = TRUE;

CwinThrad* pThread = AfxWinThread( func(), &bContinue );

// 어떤일 수행.

// 생성된 스레드의 핸들을 HANDLE변수에 저장.

HANDLE hthread = pThread‐>m_hThread;   //m_hThread는 생성된 스레드의 핸들


BContinue = FALSE;

::WaitForSingleObject( hthread, INFINITE );  //졸라리 중요한 함수임다


// 스레드 B

UINT func( LPVOID pParam )

{

     BOOL* pContinue = (BOOL*) pParam;

     While( *pContinue )

     {

             // 어떤일 수행.

     }

     return 0;

}


WaitForSingleObject란 Win32함수의 프로토타입은 다음과 같다.

     DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilisecond );

첫번째 인자로 주어진 핸들에 위의 예문에서 실행시킨 스레드의 핸들값을 저장한다. 그리고 두번째 인자로 시간값을 받는데, 이 값은 어떤 행동이 일어나기까지 기다릴 시간을 말한다. 이 함수의 목적은 첫번째 핸들값으로 주어진 그 핸들에 대한 해당하는 어떤객체(?)가 “신호를 받은 상태” 가 될 때까지 기다린다. 예제를 보면 스레드A가 스레드B를 종료시키려고 종료플래그를 FALSE로 셋팅하고 waitforsingleobject()를 호출하고 있는데, 이때 신호를 받아야하는 객체로 스레드B의 핸들을 지정하는 것이다. 이 말은 스레드 B가 정상적이든, 혹은 종료신호가 올 때까지 기다린다는 의미이다.

네트웍 프로그램에서 어떤 파일을 받는 스레드가 있고, 이 받은 데이터로 먼가를 작업하는 스레드 2개가 존재한다면 모든 데이터가 다 받아 질 때까지 받은 데이터로 작업하는 스레드는 멈춰 있어야한다. 이 경우 저 함수를 이용해서 받은 데이터로 작업을 수행하는 스레드를 멈춰 둠으로써 안정적으로 작동하게 하는 것이다.


이때 WaitForSingleObject에 두번째 인자에 INFINITE를 넣지 않고 5000이라는 값을 넣으면 무한정 기다리는 대신 그 스레드가 신호를 받기를 기다리는 시간을 5초를 지정한 겁니다. 여기서 5초가 경과된 후에도 이 객체가 신호를 받지 못하면, WaitForSingleObject는 반환이 되는데 이 반환된 값을 가지고도 프로그래머가 스레드의 상황을 파악할 수 있다.

     WAIT_OBJECT_0 : 객체가 신호를 받았음

     WAIT_TIMEOUT   : 객체가 신호를 받지 못했음


//스레드 A(받은 데이터를 가지고 어떤 작업을 한다..)

………

if(::WaitForSingleObjcet(hThread, 5000) == WAIT_TIMEOUT )

{

     AfxMesageBox(“5초가 지나도록 데이터 못 받았다..에러다..”)

}

else

{

     //데이터 다 받았으니까..알아서..해라..

}


//스레드 B(데이터를 받는다..)

…작업중…


다른 방법으로는 WaitForSingleObjct의 두번째 인자에 0을 지정함으로써 이 스레드 함수가 실행중인지 혹은 종료가 되었는지도 알 수 있다.


if( ::WaitForSingleObjcet( hThread, 0 ) == WAIT_OBJCET_0 )

{

// 스레드가 종료가 됬당..
}

else

     // 스레드가 아직 실행중이다..

즉 두 번째 인자에 0이란 값을 주면, 그 스레드에 대한 상태를 바로 반환해 주게 된다.


CwinThread*의 멤버변수중에 하나인 m_bAutoDelete를 false로 설정해 놓지도 않고, CwinThread가 신호를 받았는지 알기위해서 WaitforSingObject를 호출하는 것은 말이 안됩니다. 이유는 그 스레드가 끝이나면 해당 CwinThread*객체 또한 자동으로 사라지기 때문에, m_bAutoDelete를 false로 해주지 않으면, 말이 안된다.


③ ================================================================================

마지막 방법은 ::TerminateThread( pThread‐>m_hThread , 0 ); 이다.

이것은 어떤 스레드에서 다른 스레드를 종료시킬 때 최후의 수단으로 쓸 때 요긴하다. 위의 함수를 사용하면 pThread‐>m_hThread를 종료하고, 그 스레드에 종료코드 0을 할당한다. 이 함수는 사용함에 있어 제약이 많이 따른다.

예를 들어 동기화 객체를 예로 들수 있는데, 어떤 동기화 객체를 공유하는 스레드들이 있는데.. 어떤 스레드가 공유하는 동기화 객체를 Locking하고 작업을 수행하던중,(이때 다른 스레드들은 공유 동기화 객체를 얻기 위해 대기상태에 있다) 위의 TerminateThread함수를 사용해서 작업중인 스레드를 죽이면, 공유된 동기화 객체를 UnLocking하기 전에 죽어 버릴 수 있게 된다. 이러면 공유된 동기화 객체는 영원히 Locking되어 있기 때문에 그 동기화 객체를 공유하는 다른 스레드들은 실행이 안 되게 된다.

 

 

  등록일 : 2014-06-16 [19:50] 조회 : 3116 다운 : 0   
 
△ 이전글ppt 파일
▽ 다음글Thead 죽이기
자료구조 팁
번호 제 목 이름 조회 등록일
17 (chatGPT) 구조체 local 변수 0으로 초기화 방법 함수 정성훈 12 11-28
16 이진탐색트리에서 삭제연산 설명 함수 정성훈 939 05-30
15 insert_node 실행 예제 함수 정성훈 1056 05-09
14 └❶ ppt 파일 함수 정성훈 871 05-09
13 lThead 의 사용 함수 정성훈 1456 06-10
12 └❶ lThead 죽이기 함수 정성훈 3116 06-16
11 └❶ lThead 의 개념 동작 정성훈 1805 06-10
10 └❶ l쓰레드 동기화(커널모드1. Mutex) 함수 정성훈 2006 06-10
9 음악 플레이 프로그램 함수 정성훈 1523 06-08
8 lgetchar(), getch(), getche() 차이 함수 정성훈 1388 10-19
7 fflush 사용이유 및 방법 함수 정성훈 1270 06-19
6 linked list 의 head pointer는 malloc이 필요하지 않음 동작 정성훈 1452 05-02
5 double pointer의 사용 동작 정성훈 1409 04-15
4 파일에 쓰기 및 파일에서 읽기 함수 정성훈 1452 04-01
3 하노이탑 call 상황 H(4,A,B,C) 동작 정성훈 1537 03-25

[1][2]