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

C++빌더 강좌/문서
C++Builder Programming Tutorial&Docments
[214] 멀티쓰레드 하에서 효과적으로 메모장에 Text 출력 기법.
김태선 [cppbuilder] 23796 읽음    2010-07-27 16:19
멀티쓰레드 프로젝트가 있습니다.
각각의 쓰레드에서 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 에러가 납니다.
그러므로 종료 전에 출력과 관계된 쓰레드에 모두 정지시키는 작업이 선행되어야 합니다.
너무 상식적인 일인데, 코드 이해 없이 붙여넣기 신공을 쓴 경우 무식한 불평으로 이어지기도 하죠.



첨부한 파일에는 사용된 코드가 다 들어 있습니다.
이 코드는 사실 신뢰성 높은 프로그램이나, 상용 프로그래밍에 사용해도 전혀 하자가 없을 정도의
높은 신뢰도를 가지고 있습니다.
이 코드는 실무에 쓰일 가능성이 높으니 꼭 제대로 숙지하는게 좋을 것 같습니다.

필자가 이번 강의하는 내용은 깊이 있게 생각해 봐야 하는 문제라서,
현장에서 급하게 프로젝트를 하다 보면 미쳐 구현할 정신이 없어서 단순 무식하게 코드를 만드는 경우가 생기는데,
이런 문제는 한가할 때 구현해서, 나중에 필요할때 붙여 넣기 신공으로 써 먹는게 좋겠죠.


그럼.
외랑 [jaehuns]   2010-07-30 12:51 X
클클클..(사악한 웃음)

Log용으로 사용할 거 하나 만들어야 하는데 하면서 고민하고 있던 1인...

오 예~ 노다지를 발견... 갖다쓰기 신공을 발휘하렵니다..
무쟈게.. 감사...
초보대왕 [sauron]   2010-08-04 08:34 X
좋은 소스입니다. 그런데

-- 1. 출력 줄이 붙는다던지 엉키는 현상이 없어졌습니다. 완벽한 직렬화 동기화가 된 것입니다.
-- 2. 화면이 전혀 얼지 않습니다. 너무나 자연스럽게 다른 컨트롤이나 동작이 가능합니다.
-- 3. CPU 부하도 최소로 씁니다.
-- 4. 출력 속도도 매우 빠릅니다.
-- 5. 출력 대상을 바꾸는 것도 아무런 문제가 없습니다.
-- 6. 잘 객체화 되어 있어, 메인쪽 코드 운영이 깔금해 집니다.

-- 전체적으로 동작이 우아 아트 합니다.(웬 자화자찬)

이 중  "3. CPU 부하도 최소로 씁니다." 라는 부분은 좀 문제가 있읍니다. 로그에 남길 문자열이 없다면,
쓰레드는 로그 문자열이 발생할 때 까지 대기하는 것이 좋지만, 저 소스에서는 DisplayInterval 밀리초씩
쉬면서 계속 로그 문자열이 있는지 조사하고 있읍니다. 즉 CPU 를 많이는 아니지만 어쨌든 낭비하고 있습니다.
이런 부분은 멀티쓰레딩 전문가한테 욕먹을 수 있는 부분입니다.
유진양 [ujinyang]   2010-10-07 01:27 X
DisplayText 쓰레드에서 TForm1::DisplayMsg 함수를 호출하여 Memo1->Lines->Add 함수가 GUI를 업데이트하도록 하고 있는데, 이는 좋은 방법이 아닙니다. GUI갱신은 MainThread에서만 처리하도록 하여야 합니다. Synchronize 함수를 사용하면, MainThread에서 실행될때까지 해당 Thread가 잠시 대기하게 할 수 있어 유용하게 사용할 수 있습니다.
참치잉... [kidary97]   2011-11-17 18:05 X
와우.. 멀티 쓰레드이용해서 하드웨어 제어했어야 했는데... 이런 극강의 강좌를 주시다니....

진짜 노다지~ 발견...ㅋㅋ 소스 이해 후, 저한테 맞게 변형해서 잘 쓰겠슴다... 감솨...
김태선 [cppbuilder]   2011-12-21 18:09 X
이 코드는 제가 실전에서 로그를 남기는 용도로 잘 쓰고 있습니다.
하지만 이 코드는 실전에 쓰려면 약간의 주의를 해야 하는데,
이건 그냥 원리 강좌 용도라서 실전본을 올리지 않고 강의용 코드를 그냥 두겠습니다.
멀티쓰레드하에서 엄청나게 많은 로그가 쏟아져도 1년 넘게 끄덕없이 쓰고 있는 완벽한 코드의
초기 본입니다.
하지만 원리를 제대로 모르고 그냥 갖다 쓰려고 하면 낭패를 볼수 있으니 유의하시기를.
아루스 [tinydew4]   2013-09-05 13:31 X
Synchronize 없이 UI 자원을 건드리면 알 수 없는 문제가 발생할 수 있습니다.
물론 문제가 발생한 것을 모를 수도 있죠.
경험한 바로는 프로그램 시작시에 그런 위반을 하였는데 프로그램 종료할 때 Access violation 이 나온적이 있었습니다.
처음 이 문제의 코드를 작성하였을때는 문제가 없었는데 다른 작업을 하다보니 오류가 나더군요.
메인 쓰레드가 가지고있는 UI 자원에 맘대로 접근했을 때의 현상은 제각각이라서 모르고 지나갈수도 있죠.
김달구 [popmantion]   2013-12-17 10:47 X
초보로서 이렇게 공부할수 있고 유용한 소스를 강좌 해주셔서 진심으로 감사드립니다
어쨌든 다른 분들의 댓글도 아직 잘 이해를 못하지만 열심히 공부해서 꼭 잘 사용하는 걸로 보답하겠습니다^^

+ -

관련 글 리스트
214 멀티쓰레드 하에서 효과적으로 메모장에 Text 출력 기법. 김태선 23796 2010-07-27
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.