멀티쓰레드 프로젝트가 있습니다.
각각의 쓰레드에서 Text 메시지를 마구 쏟아내는데 표시용으로 폼의 메모장을 사용합니다.
UI의 메모장은 1개이고 이 곳으로 수십개의 쓰레드에서 메시지가 마구 쏟아져 들어옵니다.
그러면 어떻게 될까요?
한번 해 볼까요.
빈 폼에 메모장 하나 버턴 하나 올려 놓고 다음과 같이 코딩합니다.
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
class CCrazy : public TThread
{
int Index;
int Count;
public:
__fastcall CCrazy(int index, bool CreateSuspended) : TThread(CreateSuspended)
{
Index = index;
Count = 0;
FreeOnTerminate = true;
}
void __fastcall Execute()
{
while(!Terminated)
{
char msg[100];
wsprintf(msg, "[#%d] The Thread has number %d", Index, Count);
Form1->Memo1->Lines->Add(msg);
Sleep(1);
Count++;
if (Count > 1000)
{
Terminate();
}
}
}
};
for(int c = 0; c < 10; c++)
new CCrazy(c, false); // 테스트 쓰레드 10개 동시 기동
}
//---------------------------------------------------------------------------
실행하면 화면에 마구 메시지가 올라갑니다.
그런데 좀 이상하죠.
메시지가 서로 붙는 문제가 있고, 화면이 락이 걸린 듯 반쯤 얼어 있고,
CPU 사용율도 높습니다. 출력 속도도 느립니다.
이런 방식으로 출력하는 것을 단순 무식 크레이지한 방법이라고 하진 않지만,
상용 프로그램이나 신뢰성이 있는 프로그램에는 쓰면 곤란하겠죠.
물론 이런식의 화면이 마구 올라가는 프로젝트는 아마도 거의 없을 것이고,
멀티쓰레드라면 주로 로그 파일을 남기고 화면 출력은 꺼 놓는 경우가 대부분일 것입니다.
그렇지만 이런 경우도 대비할 줄 아는게 현명한 프로그래머의 길일 것입니다.
지금 화면 출력에서 줄이 겹쳐진 정도는 양반이고, 최악의 경우 내용이 사라지거나 화면이 엉망이
될 수도 있습니다. 메모장의 경우는 덜 해 보여도 만일 RichEdit 나 다른 출력용 컨트롤을 쓰는 경우
문제가 더 심각해질 수도 있습니다.
그래서 이 문제를 개선해 보도록 하겠습니다.
우선 준비해야 할 것은 멀티쓰레드 각각의 쓰레드에서 메시지를 마구 쏟아 내는데
이를 하나로 직렬화 해주는 클래스가 필요합니다.
즉 메시지가 오는 순서대로 차곡차곡 쌓아둘 수 있는 큐 클래스가 필요합니다.
다음으로 이 메시지를 화면으로 일정한 간격으로 출력해 줄 수 있는 출력용 쓰레드 클래스가 필요합니다.
그리고 이 두 클래스는 일반화 시켜 특정한 컨트롤이나 폼에 종속되지 않게 합니다.
출력용 쓰레드 클래스가 큐 클래스를 관리해서, 개발자 입장에서는
출력용 쓰레드 클래스만 다루면 되도록 하는게 좋겠죠. 그래야 인터페이스가 단순해지고
객체의 독립성이 강해 집니다.
큐 클래스는 메시지를 한줄 한줄 오는 순서대로 쌓아야 하므로, 엉키지 않게 크리티컬 섹션을 통해
동기화를 하고, 내용은 TStringList 에 쌓도록 합시다.
줄 단위의 Text를 쌓는데는 TStringList 만큼 편리한게 드뭅니다.
단 한가지 흠이라면 메모리 할당 해제가 빈번하므로 극강한 퍼포먼스를 요구하는 프로젝트라면
Text를 일정한 메모리 영역에서 관리하는 보다 빠른 클래스를 만들어 주는게 좋습니다.
하지만 상용 프로그램이라도 특별히 극강한 퍼포먼스를 요구하는게 아니라면 TStringList로도 성능이 충분합니다.
대략 설계 아이디어는 이러니 실제 구현해 봅시다.
#ifndef __CDisplayMemoText_H
#define __CDisplayMemoText_H
//---------------------------------------------------------------------------
// 멀티쓰레드에서 Text내용 순차 출력하기.
//
// Written by kTS
//---------------------------------------------------------------------------
// Text의 순차 출력용 쓰레드.
class CDisplayMemoText : public TThread
{
private:
// Text 보관용 큐 클래스
//
class CTextQueue
{
private:
TStringList *List;
CRITICAL_SECTION CS;
public:
CTextQueue()
{
List = new TStringList;
InitializeCriticalSection(&CS);
}
~CTextQueue()
{
DeleteCriticalSection(&CS);
delete List;
}
void Clear()
{
EnterCriticalSection(&CS);
List->Clear();
LeaveCriticalSection(&CS);
}
void Add(String& s)
{
EnterCriticalSection(&CS);
List->Add(s);
LeaveCriticalSection(&CS);
}
String GetText()
{
if (List->Count == 0)
return "";
EnterCriticalSection(&CS);
String text = List->Text;
List->Clear();
LeaveCriticalSection(&CS);
return text;
}
};
public:
typedef void (__closure *TDisplayMethod)(String& text);
private:
CTextQueue TextQueue;
TDisplayMethod DisplayMethod;
int DisplayInterval;
bool bPause;
public:
__fastcall CDisplayMemoText() : TThread(true)
{
bPause = false;
DisplayMethod = NULL;
FreeOnTerminate = true;
}
__fastcall ~CDisplayMemoText()
{
;
}
void SetDisplayMethod(TDisplayMethod display_method, int interval)
{
DisplayMethod = display_method;
DisplayInterval = interval;
}
void __fastcall Execute()
{
while(!Terminated)
{
if (!bPause)
{
String text = TextQueue.GetText();
if (DisplayMethod)
DisplayMethod(text);
}
Sleep(DisplayInterval);
}
}
void Add(String s)
{
TextQueue.Add(s);
}
__property bool Pause = { read = bPause, write = bPause };
};
#endif
CTextQueue 클래스는 단순히 메시지를 TStringList 에 쌓다가
GetText 메소드 요청이 오면 가지고 있는 Text를 돌려주고 TStringList 내용은 클리어 해버립니다.
이 과정에서 멀티쓰레드에서 데이타가 겹치는 현상이 생기지 않도록 크리티컬 섹션을 적용해 줍니다.
CDisplayMemoText 는 TThread에서 상속 받아서 동작합니다.
일정시간 간격으로 루프를 돌면서 CTextQueue 큐 클래스에 들어온 내용을 화면에 출력할 수 있도록
사용자 지정 메소드를 실행해주는 역할을 합니다.
그리고 자기 역할이 끝나면 스스로 사라지게 합니다.
코드가 단순해서 이해에 어려움이 없으리라 생각됩니다.
그러면 실험을 해 볼까요?
//---------------------------------------------------------------------------
TFILE Log("Log.txt", "wt");
CDisplayMemoText *DisplayText = new CDisplayMemoText;
void TForm1::DisplayMsg(String& s)
{
if (s != "")
{
Log.fprintf(s.c_str());
s[s.Length()-1] = 0; // 마지막줄의 CRLF 삭제. 그래야 메모장에서는 줄간 간격이 떨어지지 않게 찍힌다.
Memo1->Lines->Add(s);
}
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
// 메시지줄을 마구 생성할 테스트용 쓰레드
//
class CTest : public TThread
{
int Index;
int Count;
public:
__fastcall CTest(int index, bool CreateSuspended) : TThread(CreateSuspended)
{
Index = index;
Count = 0;
FreeOnTerminate = true;
}
void __fastcall Execute()
{
while(!Terminated)
{
char msg[100];
wsprintf(msg, "[#%d] The Thread has number %d", Index, Count);
DisplayText->Add(msg);
Sleep(1);
Count++;
if (Count > 1000)
{
Terminate();
}
}
}
};
DisplayText->SetDisplayMethod(DisplayMsg, 100);
DisplayText->Resume(); // 순차 Text 화면 표시용 쓰레드 기동.
for(int c = 0; c < 10; c++)
new CTest(c, false); // 테스트 쓰레드 10개 동시 기동
}
//---------------------------------------------------------------------------
좀 전에 실험했던 단순 무식한 코드와 거의 같습니다.
각각의 쓰레드에서는 단지 메시지를 직접 메모장에 출력하는게 아니라 DisplayText->Add(msg); 로
출력용 클래스에 데이타를 넣기만 합니다.
그리고 출력용 쓰레드 클래스가 기동할 수 있게 하는데,
기동시 출력용 사용자 함수와 출력 인터벌을 지정해 줍니다.
DisplayText->SetDisplayMethod(DisplayMsg, 100);
그리고 나서 바로 기동 시키고,
DisplayText->Resume();
그리고 10개의 쓰레드를 생성해 메시지를 마구 뿌리도록 합니다.
거의 극강한 수준의 메시지 폭탄을 던지는 것이나 다름 없습니다.
출력용 쓰레드 클래스는 일정한 주기로 사용자가 지정한 메소드인,
void TForm1::DisplayMsg(String& s);
를 호출해 주기 때문에 여기에서 로그 파일도 남기고
메모장에 출력도 해 줍니다.
(강좌속의 팁:
DisplayMsg(String& s); 메소드 안에 보면
s[s.Length()-1] = 0;
코드가 있는데 이는 마지막줄의 CRLF를 제거하는 코드입니다.
이를 제거해주지 않으면 메모장은 빈줄을 한줄 더 삽입하기 때문에 이를 없애기 위해서입니다.
무슨 뜻인지 감이 정확하게 안오면 위 코드를 주석처리해서 실행해 보면 압니다.
);
자 그러면 실행을 해 볼까요?
좀 전의 단순 무식한 코드의 실행결과와 비교해 보죠.
1. 출력 줄이 붙는다던지 엉키는 현상이 없어졌습니다. 완벽한 직렬화 동기화가 된 것입니다.
2. 화면이 전혀 얼지 않습니다. 너무나 자연스럽게 다른 컨트롤이나 동작이 가능합니다.
3. CPU 부하도 최소로 씁니다.
4. 출력 속도도 매우 빠릅니다.
5. 출력 대상을 바꾸는 것도 아무런 문제가 없습니다.
6. 잘 객체화 되어 있어, 메인쪽 코드 운영이 깔금해 집니다.
전체적으로 동작이 우아 아트 합니다.(웬 자화자찬)
주의할 것은, 이 쓰레드가 동작하고 있는 중에 출력용 메시지가 발생하고 있다면
이때 프로그램을 끝내려고 하면 출력 대상 폼을 잃게 되어 Access vioration 에러가 납니다.
그러므로 종료 전에 출력과 관계된 쓰레드에 모두 정지시키는 작업이 선행되어야 합니다.
너무 상식적인 일인데, 코드 이해 없이 붙여넣기 신공을 쓴 경우 무식한 불평으로 이어지기도 하죠.
첨부한 파일에는 사용된 코드가 다 들어 있습니다.
이 코드는 사실 신뢰성 높은 프로그램이나, 상용 프로그래밍에 사용해도 전혀 하자가 없을 정도의
높은 신뢰도를 가지고 있습니다.
이 코드는 실무에 쓰일 가능성이 높으니 꼭 제대로 숙지하는게 좋을 것 같습니다.
필자가 이번 강의하는 내용은 깊이 있게 생각해 봐야 하는 문제라서,
현장에서 급하게 프로젝트를 하다 보면 미쳐 구현할 정신이 없어서 단순 무식하게 코드를 만드는 경우가 생기는데,
이런 문제는 한가할 때 구현해서, 나중에 필요할때 붙여 넣기 신공으로 써 먹는게 좋겠죠.
그럼.
Log용으로 사용할 거 하나 만들어야 하는데 하면서 고민하고 있던 1인...
오 예~ 노다지를 발견... 갖다쓰기 신공을 발휘하렵니다..
무쟈게.. 감사...