C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 팁&트릭
C++Builder Programming Tip&Tricks
[36] 팁! 쓰레드의 기초
박지훈.임프 [cbuilder] 36620 읽음    1999-06-04 00:00
임프랍니다.. 오늘도 아침과 함께 시작되는 오늘의~~~~ 팁!
오늘은, 여기저기 써먹을만한 활용도가 무척 많은 쓰레드! 에 대해서 알아봅시다.


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

쓰레드.. 아직 윈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++빌더에서 생성되는 코드입니다.

unit2.h
//---------------------------------------------------------------------------
#ifndef Unit2H
#define Unit2H
//---------------------------------------------------------------------------
#include 
//---------------------------------------------------------------------------
class TImpThread : public TThread
{
private:
protected:
    void __fastcall Execute();
public:
    __fastcall TImpThread(bool CreateSuspended);
};
//---------------------------------------------------------------------------
#endif


unit2.cpp
//---------------------------------------------------------------------------
#include 
#pragma hdrstop

#include "Unit2.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------

//   Important: Methods and properties of objects in VCL can only be
//   used in a method called using Synchronize, for example:
//
//      Synchronize(UpdateCaption);
//
//   where UpdateCaption could look like:
//
//      void __fastcall TImpThread::UpdateCaption()
//      {
//        Form1->Caption = "Updated in a thread";
//      }
//---------------------------------------------------------------------------

__fastcall TImpThread::TImpThread(bool CreateSuspended)
    : TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
void __fastcall TImpThread::Execute()
{
    //---- Place thread code here ----
}
//---------------------------------------------------------------------------


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

Unit2.pas
unit Unit2;

interface

uses
  Classes;

type
  TImpThread = class(TThread)
  private
    { Private declarations }
  protected
    procedure Execute; override;
  end;

implementation

{ Important: Methods and properties of objects in visual components can only be
  used in a method called using Synchronize, for example,

      Synchronize(UpdateCaption);

  and UpdateCaption could look like,

    procedure TImpThread.UpdateCaption;
    begin
      Form1.Caption := 'Updated in a thread';
    end; }

{ TImpThread }

procedure TImpThread.Execute;
begin
  { Place thread code here }
end;

end.


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

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

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

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


뭘 해보까
─────

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


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

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

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

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

__fastcall TImpThread::TImpThread(bool CreateSuspended)
    : TThread(false)
{
    Priority = tpTimeCritical;
}

생성자에서 베이스 클래스인 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++에서는,
void __fastcall TImpThread::Execute()
{
    qq = 0;
    while(!Terminated)
    {
        qq++;
        if(qq%100==0)
            Form1->Caption = qq;
    }
}

델파이에서는,
procedure TImpThread.Execute;
begin
  qq := 0;
  while not Terminated do
  begin
      Inc(qq);
      if qq mod 100 = 0 then
          Form1.Caption := IntToStr(qq);
  end;
end;


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

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


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

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

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

다음과 같이 함수를 추가합니다. 클래스 멤버니까 당연히 클래스 선언 내에 함수 선언도 해주어야겠지요.
C++에서는,
void __fastcall TImpThread::ShowStatus(void)
{
    Form1->Caption = qq;
}

델파이에서는,
procedure TImpThread.ShowStatus;
begin
  Form1.Caption := IntToStr(qq);
end;


그리고 Execute() 내에서 폼에 바로 접근하는 부분도 Synchronize()를 호출하도록 고쳐주어야 합니다.
// C++
void __fastcall TImpThread::Execute()
{
    qq = 0;
    while(!Terminated)
    {
        qq++;
        if(qq%100==0)
            Synchronize(ShowStatus);
    }
}

// 델파이
procedure TImpThread.Execute;
begin
  qq := 0;
  while not Terminated do
  begin
      Inc(qq);
      if qq mod 100 = 0 then
          Synchronize(ShowStatus);
  end;
end;


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


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

프로젝트의 메인폼인 Form1에 버튼 두개만 따캉~! 따캉~! 하고 놔봅시다.
그리고 각각 캡션은 "시작!", "종료!" 라고 붙입니다. 그런후에..
"시작!"이라고 캡션을 넣은 Button1을 더블클릭해서 OnClick 핸들러를 만들고,
그안에 코드를 작성합시다.
// C++
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    ImpThread = new TImpThread;
}

// 델파이
procedure TForm1.Button1Click(Sender: TObject);
begin
  ImpThread := TImpThread.Create;
end;


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

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

그리고.. 이번엔 Button2("종료!"라고 캡션을 준 버튼)을 더블클릭해서 핸들러를 작성해봅시다.
// C++
void __fastcall TForm1::Button2Click(TObject *Sender)
{
    ImpThread->Terminate();
    delete ImpThread;
}

// 델파이
procedure TForm1.Button2Click(Sender: TObject);
begin
  ImpThread.Terminate;
  ImpThread.Free;
end;


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

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

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


결 론
───

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

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


오늘도 역시 상당히 길어졌군요. 글쓰는데만 두시간 정도 걸렸습니다.
다 썼지만.. 다시 한번 검토해보고.. 혹 빼먹거나 실수한 부분이 없는지 보고 하면.. 에구~

그럼.. 도움되시길 바라면서, 이만...


독립문에서 임펠리테리였습니다.
김태우 [withtw]   2004-06-08 09:07 X
감사합니다. 잘 읽었습니다.
abysmaleye [abysmaleye]   2004-08-31 11:18 X
저도 잘 읽었습니다.
다음 팁도 기대하겠습니다. ^^
BCB좋아 [junsok73]   2006-08-09 21:03 X
그대로 따라해보았습니다. 종료버튼이 안눌러집니다 ㅠㅠ
김권영 [devilica]   2007-04-10 17:26 X
Button1Click 함수 맨윗줄에

if(ImpThread != NULL)  return;

를 추가하시고

Button2Click 함수 delete 다음에

ImpThread = NULL;

를 추가해주시면 됩니다.
나그네 [kdaek]   2009-03-11 00:50 X
왜 생성자에 Priority = tpTimeCritical;(맞나?) 이걸 넣어야 하는지 알듯하네요.. tpTimeCritical 즉 실시간으로 하지 않으면 쓰레드가 작동하지 않네요. 실시간 보다 낮은 걸로 하면 말이죠.. 자바 쓰레드와 달은 빌더&델파이의 쓰레드 정말 편합니다. 흠이라면.. 등급을 지정해야 한다는거..(C#의 쓰레드도 등급지정 없이 돌릴 수 있는뎅..)
히이로유이 [hiiroyui]   2010-03-18 17:12 X
C++ 에서는 unit2.cpp 파일에서 __fastcall TImpThread::TImpThread(bool CreateSuspended)을
해더파일에서 선언 해주신거 처럼 __fastcall TImpThread::TImpThread(void)로 하고,
unit1.cpp에서도 #include "Unit2.h"추가해줘야 돌아거네요 저같은 경우에는 말이죠
박은호 [plusloveyou7]   2010-04-08 20:22 X
아.. 감사합니다.
모든게 처음이라 당황스러웠는데
좋은 팁 감사합니다.
쓰레드에 대하여 조금 더 많이 알수있는 정보 없을까요.
benpark [benny]   2012-04-03 11:03 X
글 작성일 + 13년 된 지금 글 남깁니다~ 도움 많이 됐습니다~
쭈노군 [ice0423]   2012-05-07 10:56 X
스레드 공부하는데 진짜 유익한 정보인것 같습니다. 이걸 어떻게 출력해서 보관을 할까 고민되네요 ^_^ ㅎㅎㅎ
근데 궁금한것이.. 제가 델파이 2010에서 작성해주신 내용을 따라하다보니
Synchronize(showstatus)에서 <- showstatus가 에러가나는데
showstatus를 ImpThread에 public에 두었는데 괜찮은지요..?
그리고 Synchronize를 보니 (AThread:TThread; AMethod:TThreadMethod)라고 되어 있던데
showstatus로 해서 에러가 나서 self.showstatus로 했는데
오류는 없었고 컴파일도 잘되었구요.. 이게 사용법이 맞는지 궁금합니다.
이원용 [japgo]   2014-05-19 09:10 X
본문에서

" TThread 객체의 Execute에서는 VCL의 폼 관련 루틴은 절대로 직접 접근해서는 안되며, 직접 접근할 수 있는
것은 서브 쓰레드가 아닌 프로그램의 디폴트 쓰레드(메인 쓰레드) 뿐입니다. "

라고 하셨는데, 왜 VCL폼관련 루틴에 직접 접근하면 안되는지 이유가 궁금합니다!! 어떠한 이유가 있나요?


+ -

관련 글 리스트
36 팁! 쓰레드의 기초 박지훈.임프 36620 1999/06/04
(링크)     Delphi Tip'N Tricks > 팁! 쓰레드의 기초
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.