아! 쓰레드!
──────

쓰레드.. 아직 윈32하에서, 그리고 빌더에서(혹은 델파이에서 쓰레드)를 써보지 않으신 분들은
이 쓰레드라는 녀석에 대해 상당한 본능적인 공포를 가지고 계신 분도 있을겁니다. (사실 접니다... --)
도스시절에 멀티쓰레드 어플을 만드는 것은 거의 예술의 경지였죠.
아주 가끔씩 통신망에 올라오는 '멀티쓰레드 XXX' 혹은 '멀티태스킹 XXX'를 볼때마다 (여기서 XXX를
이상하게 해석하지 마세요..) 제 눈은 존경과.. 시기와.. 경외감에 차다 못해 두려움마저... 흑흑~~~

하쥐만~! 지금은 X나 X나(포유류 이름) 맘만 먹으면 아무라도 쓰레드를 만들고 또 재밌게 갖고 놀다가
없애버릴 수 있는 아주 부담없는 개념이 되었죠. 물론 api 수준의 쓰레드 지원도 볼만합니다만... 어지러워요!
무신 함수들이 그렇게 많은지.. 헐~
그러나 vcl 차원에서 지원되는 쓰레드는, 물론 이것도 api를 래핑한 것이지만, 그래도 훨 사용하기가 편하답니다.
얼마나 사용하기 편하냐구요? 아따 성질 급하기는... 지금부터 나갑니다.


쓰레드란?
─────

먼저.. 아직 쓰레드가 뭔지를 모르실 분들을 위해, 쓰레드의 일반적인 개념에 대해 아주 간단히 살펴봅시다.
멀티쓰레드란 말을 못들어봤다고 해도, 아마 멀티태스킹이란 말은 다 아실 겁니다. (컴맹도 알던데요!)

저도 원래 전산이론에 빠삭한 도사는 못되기 때문에 정확한 정의는 내리기 힘들고, 일반적으로 멀티쓰레드란
말은 멀티태스킹이란 말과 같은 의미로 쓰입니다. 다시말하면? 쓰레드란 하나의 작업이라고 해석할 수 있다는 거죠.
멀티태스킹이란 말은 동시에 여러개의 어플리케이션을 실행할 수 있다는 뜻이 아닙니다!
사실 멀티태스킹이란 말은 여러개의 '어플리케이션' 이 아닌 여러개의 '작업'을 할 수 있다는 거죠.
말장난 같죠? 그런데 사실입니다.

윈32 커널 하의 어플리케이션은 모두 하나 이상의 쓰레드로 구성되어 있습니다. '하나 이상'이라고 한
것은, 모든 어플리케이션은 디폴트로 하나의 쓰레드가 실행되는 상태란 말입니다. 다시 해석해보면,
이 말은 디폴트로는 하나의 작업만 하지만, 어플리케이션을 설계하는 단계에서 '약간의' 수고를 더 해주면
하나의 어플리케이션이 동시에 둘 이상의 일을 할 수 있다는 뜻이 되죠.
잘 이해가 안되시면, 쉽게 생각해서 하나의 어플리케이션 안에 또하나의 어플리케이션을 돌릴 수 있다고
생각해도 되겠네요.


단계1 : 쓰레드 클래스의 뼉다구를 만들자
────────────────────

C++빌더와 델파이의 쓰레드는, 물론 api 수준의 쓰레드함수들을 그대로 쓸수도 있지만, 역시 oop 언어답게
TThread 클래스가 미리 준비되어 있죠! 요 귀여운놈을 상속받아서 클래스를 구축하면 됩니다.

하지만 이걸 매번 직접 코드를 만들려면 귀찮으니까... C++빌더/델파이의 IDE에서 기본 뼉다구(skeleton)
코드를 만들어주는 기능이 있습니다.
델파이 혹은 C++빌더의 File 메뉴의 가장 위에 있는 New 항목을 클릭해서 나오는 New Item 다이얼로그에서
첫번째 탭에 있는 아이콘들 중에서(여기 보면 어플리케이션 , 컴퍼넌트, 폼, dll, 유닛 등의 항목이 있죠)
'Thread object'가 있답니다. 당근 이걸 클릭! 그럼 다이얼로그가 뜨면서 델파이 혹은 C++빌더가 물어봅니다.
'만들 쓰레드클래스의 이름은 뭘로 할거유?...'
역시 어제와 마찬가지로... 학습의 효과를 높이기 위해, 이름을 이렇게 붙여봅시다.
ImpThread. 조오쵸?

이렇게 해서 만들어진 코드가 다음과 같습니다. 자세히 보실 필요는 없고..
그냥 TThread를 상속받고 있다는 것, 그리고 멤버함수가 뭐뭐인지 정도만 봐두세요.
다음은 C++빌더에서 생성되는 코드입니다.



델파이에서 생성되는 코드는 다음과 같습니다.


사족 : 얼마나 갈켜줄까
───────────

자아.. 이렇게 해서 길쭈구리한 코드들을 포함한 새 유닛이 자동생성되었습니다.
생성됐으면? 그걸로 끝인가? 써먹어야지! ^^

사실 이 쓰레드라는 개념은 자그마한 팁이 아니라, 꽤나 복잡한 기술이기도 하고, 또 활용도도 말할 수도 없이
많습니다. 글구 이 TThread 클래스의 메소드와 프로퍼티만 해도 한두개가 아니랍니다. 그럴수 밖에 없는 것이,
sdk의 그 많은 쓰레드 관련 api를 대부분 클래스화했으니...

그럼 임프는 오늘 과연 이 쓰레드에 관련한 모든 것을 다 갈쳐줄수 있을까요?
대답은... 택도 엄따~! 입니다. ^^;;;
쓰레드를 제대로 설명하려면 분량도 웬만한 책 한권은 나올정도인데다가, 무엇보다도... 저도 다 모릅니다! (쿵~!)
그래서.. 이러이러한 관계로.. 오늘은 아주~! 간단한, 하지만 작은 예제 하나를 들어보면서 워밍업만 해봅시다.



뭘 해보까
─────

뭘 하느냐... 아 기다려봐요~
이런 경험이 있을겁니다. for문을 돌리면서 메시지를 처리하려면?
for 루프를 돌고 있는 동안에는 어플리케이션 전체가 busy 상태이므로 특별한 처리를 해주지 않으면 어플리케이션에
넘어오는 메시지들은 전혀 처리되지 못한채 메시지큐에 차곡차곡 쌓이죠. 이 상태가 바로 '응답없음' 상태랍니다.
보통은 이걸 간단히 해결하기 위해 for 루프 내에 Application->ProcessMessages()를 삽입하는데..
이 메소드는 잠시 현재 작업을 잠시 중지시켜놓고 메시지큐에 쌓인 메시지들을 처리하는 역할을 하죠.
문제는.. 이 방식이 상당히 느리단 거죠. 거기다 for 루프내의 코드가 상당히 복잡할 경우 몇번씩이나 같은
Application->ProcessMessages()를 삽입해야 하고.. 느리니까 당연히 시간 정확도가 요구되는 작업에는 쓰면
안됩니다. 이럴때 바로 쓰레드를 씁니다. 물론 딴데도 쓰지만, 여기서 쓰레드의 활용도를 간단한 예제로 설명하는데는
아주 딱이겠네요.


단계 2 : 쓰레드 클래스의 생성자 작성
──────────────────

자아.. 그럼 코드를 작성해봅시다. 아까 만든 ImpThread 그대로 있죠?
거기다 장난을 쳐봅시다.

C++빌더에서는, 헤더에 있는 생성자 선언을 조금 수정해줍니다.
__fastcall TImpThread(bool CreateSuspended);
에서,
__fastcall TImpThread(void);
이렇게 바꿉니다.

마찬가지로, 생성자의 바디 부분도 조금 수정하고 코드 한줄을 추가합니다.


생성자에서 베이스 클래스인 TThread 생성자의 인자로 false를 넘겨주도록 수정한 것을 봐두세요.
TThread의 생성자가 갖는 CreateSuspended 인자는 쓰레드 객체만 생성해두고 실제로 쓰레드를 실행하지는 않고
대기할 것인가의 여부를 결정합니다. 따라서 false를 넘겨주면 생성된 직후에 바로 실행이 시작됩니다.

한편, 델파이에서는...
똑같이 쓰레드 오브젝트를 생성해도, C++빌더에서는 생성자 코드까지 만들어주지만 델파이에서는 생성자를
만들어주지 않습니다. 그러니까 델파이를 쓴다면 생성자 선언과 바디를 직접 만들어줘야 하지요.

interface 섹션의 TImpThread 클래스 선언에, public을 추가하고 다음과 같이 생성자를 코딩해줍니다.
constructor Create;

그리고 위에서 선언한 생성자의 바디를 implementation에 추가합니다.
constructor TImpThread.Create;
begin
  inherited Create(false);
  Priority := tpTimeCritical;
end;

베이스 클래스인 TThread의 생성자로 false를 넘겨준 것은 위의 C++의 경우와 같은 이유입니다.
그리고 역시 똑같이 한줄 더 추가했습니다.

뼉다구 코드에다가 딱 한줄 추가한겁니다. 그쵸?
여기서 Priority는 TThread 클래스의 프로퍼티로서.. 현재 쓰레드의 작업 우선순위를 말합니다.
tpTimeCritical은 가장 높은 단계로서, 시간의 오차가 최소화되어야 하거나 윈도우즈가 아무리 바쁜 상황이라도
반드시 수행되어야 할 작업을 할때 이 tpTimeCritical을 지정합니다. 반대로 가장 낮은 순위는 tpIdle이고, 그 위로
tpLowest, tpLower, tpNormal, tpHigher, tpHighest, 그리고 tpTimeCritical까지 지정이 가능합니다.
그럼, 여기서 tpTimeCritical로 지정한 이유는? 대단한 걸 해보려고? 아닙니다.
그냥 기분이 나서 해봤습니다. 따지지 마세요.


단계 3 : 쓰레드 클래스의 Execute 메소드 작성
───────────────────────

그리고.. 이번엔 그다음 뼉다구인 Execute() 메소드의 바디에다 코드를 작성해 봅시다. 바로 이 Execute 메소드가
쓰레드 자체입니다. 별도의 쓰레드로 나누어 작업을 시킬 바로 그 작업을 이 Execute 메소드에게 시켜먹으면 되는
겁니다. (참고로 생성자나 파괴자, 그외 다른 모든 메소드들은 쓰레드로 동작하지 않으며 프로그램의 메인 쓰레드에서
동작합니다)

먼저 TImpThread 클래스의 선언에 private 섹션에 정수형 변수 qq를 하나 추가합니다. C++이라면,
class TImpThread : public TThread
{
private:
    int qq;
    ...
이렇게 될 것이고,  델파이라면,
  TImpThread = class(TThread)
  private
    qq: integer;
    ...
이렇게 되겠죠.

이제 Execute 메소드에 다음과 같이 코드를 추가해봅시다.
C++에서는,


델파이에서는,

여기서...Terminated가 뭘까요?
Terminated는 속성으로서 현재 쓰레드가 작업중인지의 여부를 가리키는 boolean형 속성입니다.
따라서 Terminated = true가 되는 순간이 현재 쓰레드의 작업을 마쳐야 할 시점이라고 생각하면 되겠습니다.

그러니까.. qq의 값을 1씩 증가시키다가 100으로 나눠 떨어지면 폼의 캡션에 그 값을 표시하고 계속 실행하라...
정도 되겠습니다. 그러면 폼1의 캡션이 미친X처럼 파라라라라락~~~ 카운팅되고 있는 숫자로 정신없는 상황이
눈에 선하죠? ^^


단계 4 : Synchronize()
────────────

여기서 쓰레드 프로그래밍에서 반드시 짚고 넘어가야 할 중요한 오류가 하나 있습니다.
바로 폼의 캡션을 수정하는 부분입니다.
TThread 객체의 Execute에서는 VCL의 폼 관련 루틴은 절대로 직접 접근해서는 안되며, 직접 접근할 수 있는
것은 서브 쓰레드가 아닌 프로그램의 디폴트 쓰레드(메인 쓰레드) 뿐입니다.

그러면, 지금 작성하고 있던 예제의 경우 폼에다가 표시하는 것이 목적인데 방법이 없으면 안되겠죠?
이럴 때 쓸 수 있는 것이 TThread 클래스의 Synchronize() 함수입니다. 이 함수는 Execute() 실행중에
반드시 실행해야 하는 루틴을 디폴트 쓰레드로 실행을 위임합니다. 필요한 루틴들을 별도의 함수로 만들어
분리한 후, 그 함수의 포인터를 Synchronize() 함수로 넘기면 된답니다.

다음과 같이 함수를 추가합니다. 클래스 멤버니까 당연히 클래스 선언 내에 함수 선언도 해주어야겠지요.

그리고 Execute() 내에서 폼에 바로 접근하는 부분도 Synchronize()를 호출하도록 고쳐주어야 합니다.


자아.. 한가지만 더 하고 이 쓰레드 유닛은 끝냅시다. 뭐냐구요? 위에서 Form1을 억세스 했잖아요.
그러니 당연히 인클루드 해야죠. 안그럼 컴파일러가 삐지겠죠? #include "Unit1.h"
이제.. 쓰레드 클래스는 다 만들었습니다. 뭐이리 간단하냐구요? 그러니까 빌더가 조은 거지!
그럼.. 이제부터 실제로 이 클래스의 객체를 생성하고 쓰레드를 시작하는 코드를 작성해봅시다.


단계 5 : 완성된 쓰레드의 사용
──────────────

프로젝트의 메인폼인 Form1에 버튼 두개만 따캉~! 따캉~! 하고 놔봅시다.
그리고 각각 캡션은 "시작!", "종료!" 라고 붙입니다. 그런후에..
"시작!"이라고 캡션을 넣은 Button1을 더블클릭해서 OnClick 핸들러를 만들고,
그안에 코드를 작성합시다.


근데 여기서 써먹은 ImpThread라는 TImpThread 객체는 어디에도 아직 만들지 않았죠? 폼과는 달리 쓰레드는
전역적으로 선언되는 포인터를 자동으로 만들어주지 않습니다. 그러니 직접 선언해줍시다. 어디다가?
그것도 일일이 갈켜줘야해요? 뭐, 폼클래스에 집어넣든지 혹은 유닛에 전역적으로 선언하든지 관계없지만,
폼클래스가 지저분한건 딱 질색이니까 폼 유닛의 위 Button1Click() 함수 바로 위에다가 선언해줍시다.
딴데 하고 싶다고요? 누가 말립니까?

TImpThread *ImpThread; // C++
ImpThread: TImpThread; // 델파이

그리고.. 이번엔 Button2("종료!"라고 캡션을 준 버튼)을 더블클릭해서 핸들러를 작성해봅시다.

뭐.. 간단하지요? 별로 설명할게 없어보입니다만.. 굳이 사족을 달자면, Terminate()는 당연히 TThread의
메소드로서, 쓰레드의 실행을 중지시키는 거죠. 그리고 마지막으로 더이상 쓸모가 없어진 쓰레드를 삭제하고요.

마지막 단계 : 실행해보잣!
─────────────

끝입니다. 설명은 장황했지만, 실제로 작성한 코드는 불과 열줄이 채 되지 않죠?
그런데도 실행해보면, 캡션은 열심히 미친 X 처럼 숫자를 카운트하고 있고, 그동안에도 마치 어플리케이션은
아무 일도 안하고 있는 것처럼 메시지를 잘 처리합니다. 대표적으로 캡션바를 드래그해보면, 바쁘게 카운팅을
하는 중인데도 불구하고 드래깅하는 마우스메시지에 제깍제깍 반응합니다. 신기하지요? ^^


결 론
───

오늘의 팁은 여기까지입니다.
요약하자면.. 사실 쓰레드는 아주 복잡한 내부 처리를 거치는 아주 큰 기술입니다. 다루기도 쉽지 않구요.
하지만 vcl에서는 이 쓰레드 개념을 적절히 래핑하여 아주 재미있게 장난감처럼 갖고놀 수 있는 클래스로
만들어놨습니다.

월요일엔 이 쓰레드를 활용하는 다른 예를 들어보겠습니다.
사실 오늘 쓰레드를 설명한 것도, 월요일에 진행할 다음 팁을 쓰는데 쓰레드 개념이 필수적이기 때문입니다.
뭔지 미리 공개하지는 않겠고.. 아주 많은 분들이 애타게 기다리는 팁이란 것만 귀띔해두죠.
벌써 여기저기에서 관련 질문만 여러번 올라왔던 전력이 있는 팁인데, 아직까지 제대로된 답변이 올라온 걸
본적이 없거든요.

// 볼랜드 포럼에서 발췌..

+ Recent posts