델파이 TThread 사용 길잡이
초보자를 위한 TThread 사용 길잡이
질답 게시판을 보다 보면 쓰레드에 관한 질문이 많이 올라 오는데,
특히 초보자분들이 쓰레드의 사용법을 잘 모르고 계신 경우가 많은 것 같아,
언제 한 번 써야겠다고 생각하고 있었는데,
오늘따라 일도 손에 잘 안 잡히고 해서 한번 정리해 봅니다....
언제 쓰레드가 필요한가?
사실 대부분의 프로그램에서는 굳이 쓰레드를 사용할 일이 없습니다.
설계 시점에서 과연 쓰레드를 써야 하는 건지를 먼저 고민해 보십시오.
타이머로도 충분히 처리할 수 있는 것을 굳이 쓰레드를 사용할 필요는 없다는 것입니다.
쓰레드 사용이 꼭 필요한 부분 중 하나는 분산처리를 필요로 하는 프로젝트입니다.
요즘의 PC들은 멀티코어가 기본입니다.
우리의 델파이는 워낙 깔끔한 툴이라 OpenMP 같은 변태적인 기능은 지원하지 않습니다.
멀티코어를 지원하는 최선의 방법은 바로 멀티 쓰레딩입니다.
압축 프로그램의 예를 들자면 그냥 하나의 쓰레드에서 압축하는 것보다
여러 개의 쓰레드를 만들어 각 쓰레드에서 파일을 하나씩 잡고 압축을 한다면,
쿼드코어 환경에서 4배까지는 아니더라도 3배는 빨라지지 않을까 싶습니다.
그리고 델파이에서 쓰레드의 수요가 가장 많을 것으로 예상되는 데이터 송수신 모듈입니다.
블로킹 소켓이나 시리얼 통신에 많이 사용됩니다.
언제 들어올지 모르는 데이터를 타이머로 체크하기는 힘듭니다.
타이머 인터벌이 길면 대응이 느려지고, 그렇다고 인터벌을 짧게 하자니 CPU 부하가 걱정 되고..
데이터 수신에는 당연히 쓰레드가 필요합니다.
그렇다면 송신은? 송신 버퍼만으로 충분히 감당할 수 있는 간단한 패킷 정도를 날린다면
아무데서나 날리면 되겠지만 아주 큰 데이터를 날린다면(파일 전송 같은) 이것도 쓰레드로 처리해야 합니다.
그 외에도 여러 필요한 부분이 있겠지만
주로 사용되는 곳이 이 정도가 아닐까 싶네요.
기본 사용법
TThread 클래스는 쓰레드는 Classes unit에 선언 되어 있으며,
쓰레드를 구현하기 위해서는
TThread 클래스를 상속받아 새로운 클래스를 만들어야 합니다.
uses Windows, Classes, Messages; type TThreadA = class(TThread) protected FOwner: HWND; procedure Execute; override; public constructor Create(Owner: HWND; CreateSuspended: Boolean); end; |
예를 들자면 이렇습니다.
이 중 새로운 쓰레드 내에서 실행되는 부분이 Execute 함수인 것입니다.
당연한 말이지만 Create 함수는 부모 쓰레드에서 실행됩니다.
이 예제에서 Owner 파라미터는 메인폼의 핸들이 전달됩니다.
constructor TThreadA.Create(Owner: HWND; CreateSuspended: Boolean); begin FOwner := Owner; inherited Create(CreateSuspended); end; |
여기서 CreateSuspended의 의미는
새로운 쓰레드를 만들고 바로 동작을 시킬 것인지
아니면 만들기만 하고 나중에 동작 시킬 것인지를 지시합니다.
보통 상속받은 클래스의 Create 함수들을 보면 inherited를 먼저 하고 다른 처리를 하는데,
위에서는 변수 초기화를 먼저하고 inherited를 호출하고 있네요.
CreateSuspended가 FALSE인 경우라 하더라도
Create 함수가 끝나기 전에 쓰레드가 시작되는 일은 없습니다.
내부적으로는 무조건 Suspend인 상태에서 쓰레드를 만들고
Create가 완전히 끝난 후에 CreateSuspended를 검사해서
자동으로 Resume을 실행하든지 말든지 하는 것입니다.
그러한 사실을 모르던 시절에, 초기화가 끝나지도 않았는데 쓰레드가 실행될까 걱정되어
저렇게 쓰던 것이 눈에 익어버려 지금도 그냥 저렇게 쓰고 있습니다.
이런 건 따라 하지 마세요.
Terminate는 쓰레드를 종료시키는 함수가 아니다.
이제 쓰레드가 시작하는 것은 알겠는데 끝나는 것은 언제일까요?
Execute 함수를 빠져나오면 쓰레드는 끝납니다.
물론 Execute함수가 끝난 후에도 쓰레드 끝나기 전에 몇 가지 더 하는 일은 있습니다만,
우리는 TThread를 상속 받는 입장이니 Execute 함수가 끝나면 쓰레드도 끝난다고 보시면 됩니다.
procedure TThreadA.Execute; var I : Integer; begin for I:=1 to 30 do begin PostMessage(FOwner, WM_USER, GetTickCount(), 0); Sleep(1000); end; end; |
위의 예제는 for문으로 30번을 돌면서 메인폼으로 메세지를 날리고 있습니다.
30번을 모두 날린 후에는 자동으로 종료합니다.
이때 종료된다는 것은 쓰레드가 종료되는 것(Terminate)이지
생성된 인스턴스가 파괴(Destroy)되는 것은 아닙니다.
FreeOnTerminate를 TRUE로 해두는 경우에는
쓰레드가 종료되면 자동으로 인스턴스도 파괴됩니다.
그렇지 않은 경우 Free 함수를 사용하여 직접 파괴 해야 합니다.
그렇다면 Terminate 함수는 왜 있는 것일까요?
Classes.pas 를 들여다보시면 이렇게 되어 있습니다.
procedure TThread.Terminate; begin FTerminated := True; end; |
여기서 FTerminated는 프로퍼티가 아닙니다.
그냥 Boolean 형 변수일 뿐입니다.
즉, 이것만으로는 쓰레드를 종료할 수가 없다는 것입니다.
procedure TThreadA.Execute; begin while not Terminated do begin PostMessage(FOwner, WM_USER, GetTickCount(), 0); Sleep(1000); end; end; |
이것이 대부분의 쓰레드가 가지는 기본 형식입니다.
(자기가 그렇게 쓴다고 남들도 다 그렇게 쓰는 줄 아는 1人)
여기서 Terminated는 FTerminated를 프로퍼티로 포장해 놓은 것이니 둘은 같은 놈입니다.
Execute가 저런 형식으로 되어 있는 경우에만
Terminate 함수를 호출했을 때 쓰레드가 종료되는 것입니다.
혹은 이렇게 할 수도 있겠습니다.
procedure TThreadA.Execute; var I : Integer; begin for I:=1 to 30 do begin if Terminated then Break; PostMessage(FOwner, WM_USER, GetTickCount(), 0); Sleep(1000); end; end; |
이 경우들에서 Terminate 함수를 호출하면 쓰레드가 종료가 되기는 합니다만,
호출하는 즉시 종료가 되는 게 아니라 한참 있다가 종료되는 현상이 발생합니다.
바로 Sleep(1000) 때문입니다.
1초 동안 잠자고 있는 관계로 운 좋으면 즉시 종료하지만,
운 나쁘면 1초 후에 종료하게 됩니다.
복불복인 것입니다.
그렇다면 해결책은?
procedure TThreadA.Execute; var I, J : Integer; begin for I:=1 to 30 do begin if Terminated then Break; PostMessage(FOwner, WM_USER, GetTickCount(), 0); for J:=1 to 10 do begin if Terminated then Break; Sleep(100); end; end; end; |
무식하지만 나쁜 방법은 아닙니다.
음...... 나쁜 방법은 아닐 겁니다....................흠.......나쁩니다..Sleep은 비추천 함수입니다....ㅠ
이제는 아무리 늦게 반응해도 0.1초 내에는 종료하게 됩니다.
이런 식으로 쓰레드 내에서 시간이 어느 정도 걸릴 것으로
예상되는 작업은 Terminated 검사를 병행 해주는 것이 좋습니다.
어느 정도 시간이 걸릴 것으로 예상되는 작업이란
위와 같은 딜레이나, WinSock.accept, Winsock.recv, 이런 것들 입니다.
긴 시간 지연에 사용할 만한 좀 더 고급스러운 방법으로는
WaitForSingleObject를 사용하는 방법이 있습니다.
단, 이벤트를 하나 생성해야 하고, 쓰레드를 종료할 때
이벤트를 발생시켜야 하는 부담이 있습니다.
Terminate 함수를 상속받아 발생시키면 되는데,
Terminate 함수는 virtual 함수가 아니라서
버그의 여지가 있다는 점을 염두에 두어야 합니다.
어느 쓰레드에서 실행되는 중인가?
새로운 멤버함수 또는 이벤트들을 설계할 때 어디서 호출이 되는 것인지,
어디서 호출을 하면 안 되는 것인지를 구별해야 합니다.
즉, 메인쓰레드에서 호출하는 것인지 생성된 쓰레드 내부에서 호출하는 것인지를
잘 구별하지 않으면 엉망이 될 수가 있으니 조심해야 한다는 것입니다.
내부에서만 호출할 것들은 private나 protected에 넣으면
외부에서 호출을 할 수 없겠지만,
public영역에 만들어지는 함수들은 리마크라도 달아놓는 것이 좋습니다.
어느 한쪽에서만 사용되는 함수라면 발생하지 않는
일이 양쪽에서 모두 사용되는 경우에는 발생할 수 있기 때문입니다.
소켓으로 패킷을 하나 전송하는 멤버함수를 하나 만들어 보겠습니다.
procedure TThreadA.SendPacket(Command: Word; ParamW, ParamL: Integer); begin WinSock.send(SockID, PACKET_HEAD, SizeOf(PACKET_HEAD), 0); WinSock.send(SockID, Command, SizeOf(Command), 0); WinSock.send(SockID, ParamW, SizeOf(ParamW), 0); WinSock.send(SockID, ParamL, SizeOf(ParamL), 0); end; |
예 맞습니다.
구조체 하나 보내면 될걸 예를 들기 위해 억지로 만든 겁니다.
아무튼 이러한 함수가 있습니다.
이 쪽에서 능동적으로 보내는 패킷은 메인쓰레드에서 보내고,
상대방의 패킷에 대한 응답 패킷은 쓰레드 내부에서 보낸다고 했을 때,
양쪽에서 동시에 이 함수를 호출하면 어떻게 될까요?
어느 한쪽이 완전히 보내기도 전에 다른 쪽이 끼어들면 패킷이 깨지게 됩니다.
이러한 사태를 막기 위해 주로 사용하는 것이 "Critical Section"입니다.
우리말로는 "임계영역"이라고 하는데,
하나의 쓰레드가 먼저 진입을 하면 이 쓰레드가 작업을 마치고 빠져나가기 전에는
다른 쓰레드가 진입하지 못하도록 막아주는 일을 하게 됩니다.
쓰레드 Create에서 InitializeCriticalSection 함수로 초기화하고,
진입 시 EnterCriticalSection, 벗어날 때는
LeaveCriticalSection,
Destory에서 DeleteCriticalSection 함수로 마무리 하면 됩니다.
procedure TThreadA.SendPacket(Command: Word; ParamW, ParamL: Integer); begin EnterCriticalSection(FCritical); WinSock.send(SockID, PACKET_HEAD, SizeOf(PACKET_HEAD), 0); WinSock.send(SockID, Command, SizeOf(Command), 0); WinSock.send(SockID, ParamW, SizeOf(ParamW), 0); WinSock.send(SockID, ParamL, SizeOf(ParamL), 0); LeaveCriticalSection(FCritical); end; procedure TThreadA.SendBuffer(Buffer: Pointer; Length: Integer); begin EnterCriticalSection(FCritical); WinSock.send(SockID, Buffer^, Length, 0); LeaveCriticalSection(FCritical); end; |
이런 식으로 소켓으로 전송하는 모든 부분에서 처리를 해야 합니다.
자 이제 위의 함수로 패킷을 보내는데,
여러 가지 패킷 중에 특수한 패킷이 있을 수도 있겠습니다.
예를 들면 파일명을 전송해야 한다고 해 봅시다.
패킷에는 파일명을 직접 넣을만한 공간이 없습니다.
그래서 ParamL에 파일명의 길이를 넣고 패킷을 보낸 후
바로 SendBuffer 함수로 파일명을 전송하도록 설계하게 됩니다.
SendPacket(PACKET_CMD_FILENAME, 0, Length(FileName)); SendBuffer(PChar(FileName), Length(FileName)); |
각 함수들이 임계영역 처리가 다 되어 있으니 문제가 없을 것 같지만,
그렇지가 않습니다.
각각은 처리가 되어 있지만 SendPacket을 마치고
SendBuffer로 진입하기 전에 다른 쓰레드에서 끼어들 여지가 있기 때문입니다.
그러한 일을 방지하기 위해서는 역시 임계영역으로 묶어야 합니다.
procedure TThreadA.SendFileName(const FileName: String); begin EnterCriticalSection(FCritical); SendPacket(PACKET_CMD_FILENAME, 0, Length(FileName)); SendBuffer(PChar(FileName), Length(FileName)); LeaveCriticalSection(FCritical); end; |
그런데, 여기서 한가지 의문이 발생할 수 있습니다.
SendFileName에서 임계영역으로 진입을 해 버리면
SendPacket의 EnterCriticalSection에서 진입을 하지 못하고 무한대기가 발생하지 않을까 하는 점입니다.
그렇지만 그런 걱정은 하실 필요가 없습니다.
이 임계영역이란 놈은 다른 쓰레드의 진입을 못하도록 막는 것입니다.
같은 쓰레드라면 열 번이든 백 번이든 진입이 가능합니다.
물론 EnterCriticalSection의 수만큼 LeaveCriticalSection이 호출되어야 빠져 나오는 것입니다.
Synchronize
동기화 (同期化) [명사] [컴퓨터] 작업들 사이의 수행 시기를 맞추는 것. 사건이 동시에 일어나거나, 일정한 간격을 두고 일어나도록 시간의 간격을 조정하는 것을 이른다. <출처: NAVER 국어사전> |
"동기화"라는 참 어려운 단어가 있습니다.
영어로는 Synchronization이라고 하는 군요.
보통은 둘이 똑같도록 만드는걸 "동기화시킨다" 라고 합니다.
PC 싱크, 싱크로율 100%, 싱크로나이즈드 스위밍 등에서 쓰일 때 그런 의미 입니다.
그런데 이상한 것은 둘이 똑같지 않도록 만드는 것도 동기화라고 부른다는 점입니다.
쓰레드에서는 둘이 동시에 같은 일을 하지 못하도록 하는 것을 "동기화시킨다" 라고 합니다.
위에서 말한 임계영역이 바로 동기화를 위한 도구입니다.
그런데, 임계영역으로 동기화 시킬 수가 없는 부분이 있습니다.
바로 VCL 입니다.
내가 만들 거라면 임계영역으로 만들겠지만,
남이 이미 만들어 놓은 건 어떻게 해볼 도리가 없습니다.
동기화 없이 GUI에 관계된 일을 수행하면 화면이 얼어붙는 프리징 현상이 나타납니다.
그렇다고 VCL 소스를 직접 고쳐 임계영역을 설정할 수도 없으니 방법은 하나뿐입니다.
GUI에 직접 관여하지 않는 것입니다.
다시 말해 VCL의 TControl 클래스로부터 상속받은 놈들은 쓰레드 안에서 사용하지 않는 것입니다.
사용할 일이 있더라도 직접 건드리지 말고 GUI와 친한 메인쓰레드에게 떠넘겨 버리도록 합시다.
시리얼 포트로 주기적으로 들어오는 데이터를 로그에 기록하고
화면에는 그래프를 그리는 작업을 한다고 가정합니다.
시리얼 통신은 쓰레드로 구현을 하게 됩니다.
쓰레드 내부에서는 들어온 데이터를 메인쓰레드로 PostMessage 함수를 사용해 전달만 합니다.
그래프는 메인 쓰레드가 그 값을 받아서 그립니다.
로그는 어느 쪽에서 기록하든 관계가 없겠습니다.
PostMessage 대신 SendMessage를 사용하는 건 어떨까요?
그건 안됩니다.
PostMessage는 메세지 큐에 메세지를 넣어놓으면 메인쓰레드가 꺼내서 처리하는 반면,
SendMessage는 큐에 넣지 않고 지가 직접 들어가서 처리하기 때문에 쓰레드가 그래프를 그리는 것이 됩니다.
이 경우는 숫자 정도만 전달하면 되니
PostMessage로 해결이 되겠지만, 전달 해야 할 값이 문자열 같이 덩치 큰 값이라면?
메모리를 할당해서 그 포인터를 넘겨주고 받는 쪽에서 처리 후 해제를 하면 됩니다.
위 방법에서는 메모리 할당과 해제가 부담이 될 수도 있겠네요.
그렇다면 그냥 로컬 변수나 클래스변수의 주소를
PostMessage로 넘길 수도 있지 않을까요?
그런데 로컬 변수를 전달한다면 메인쓰레드에서 해당 메세지가 처리되기 전까지는
현재의 함수를 빠져나가서는 안 된다는 문제가 있습니다.
클래스 변수로 전달하는 경우에는 해당 메세지가 처리되기 전까지
클래스 변수의 값이 바뀌지 않도록 관리 해야 한다는 것입니다.
그 방법은 메세지가 처리될 때까지 다른 일 안하고 기다리는 것뿐입니다.
쓰레드의 존재 이유가 무색해지는 방법입니다.
그런데, TThread에는 이 방법을 포장해놓고서 사용해주기를 기다리는 놈이 있습니다.
바로 Synchronize라는 함수입니다.
함수 포인터 하나를 메인쓰레드에 알려주고 그 함수의 실행이 끝날 때까지 파업에 돌입합니다.
Synchronize를 사용하면 안 된다는 것은 아닙니다.
사용하라고 만들어 놓은 것이데 사용하지 않을 이유는 없습니다만,
동작하는 원리를 이해하고 사용하면 문제 해결이나 예방에 도움이 되지 않을까요?
원격제어 프로그램이 하나 있는데,
쓰레드에서는 소켓으로 계속 상대방의 화면을 계속 받고 있습니다.
받아서 이걸 Synchronize로 그리면 다음과 같은 현상이 생길 수 있습니다.
화면 한 장 그리고 나니 또 다음 화면이 들어와 있고, 또 다음 화면이 들어와 있고......
즉 화면에 그리는 속도보다 화면 데이터가 더 빨리 들어오는 경우입니다.
결국 소켓버퍼에서 빨리빨리 꺼내가지 않으니 상대방에서
WinSock.send 함수에서 대기현상이 생깁니다.
원인은 터미널에 있는데 결과는 호스트에서 나타나는 거죠.
여기서 쓰레드의 역할은 소켓에서 데이터를 받아내는 것이지 화면에 그림을 그리는 것이 아닙니다.
화면을 받으면 그걸 메인쓰레드에 전달하고 또 다음 화면 받기 모드로 바로 넘어가야 하는 것입니다.
Synchronize로 처리가 안될 정도로 밀려오는 데이터를 PostMessage를 쓴다고 해결이 되겠느냐,
메세지 큐가 넘치지는 않을까라는 생각이 듭니다.
그렇지만 이건 다른 방법으로 피해갈 수 있습니다.
제가 사용한 방법은 이렇습니다.
화면을 받아서 저장하는 정적 버퍼를 2개 만들고,
하나를 PostMessage로 보내 놓고 다른 버퍼에다가 계속 받습니다.
메인쓰레드가 그림을 다 그렸다는 신호를 보내면 두 버퍼의 역할을 바꾸고......
시간이 모자라 그려지지 못한 데이터는 자동으로 덮어쓰게 되니 항상 최신 화면만 그려지게 됩니다.
물론 이외에도 대화형 패킷으로 데이터 전송을 제어하긴 하지만,
쓰레드 간의 역할을 분명히 하는 것에 대한 예제로 소개해 드린 것입니다.
Suspend와 Resume
쓰레드에서 사용시 주의를 요하는 기능이 또 있습니다.
Suspend와 Resume 입니다.
기능은 간단하게도 쓰레드를 잠시 멈추고 다시 시작하는 것입니다.
CreateThread API에 익숙한 분들이 TThread로 넘어오면
한번쯤 고민할 것으로 예상되는 문제 중에 쓰레드 강제종료 문제가 있습니다.
TThread는 쓰레드 강제종료 기능을 제공하고 있지 않습니다.
TerminateThread API로 강제 종료시키는 분들도 있겠지만,
정상적인 방법은 아닌 것 같습니다.
API로 쓰레드 작업을 하는 경우 흔히 볼 수 있는
강제종료 코드는 SuspendThread 후에 TerminateThread를 수행하는 것입니다.
이러한 습관을 TThread에도
그대로 적용시켜 Suspend 후에 Terminate를 수행하고는 쓰레드가 종료하지 않는다라고
질문하시는 분들을 게시판에서 가끔 볼 수 있습니다.
Terminate 함수가 하는 일은 위에서 나왔으니 생략하고,
Suspend가 호출되는 순간 쓰레드는 딱 멈춰버립니다.
TThread에서 쓰레드를 종료하는 유일한 방법은 Execute 함수를 벗어나는 것인데,
딱 멈추어 있으니 절대 벗어날 수가 없게 됩니다.
Terminate 시에는 오히려 멈춰있는 쓰레드를 Resume을 시켜줘야 하는데 말입니다.
TThread에서 강제종료를 지원하는 함수가 없는 것은
강제종료를 하지 말라는 뜻이라고 보시면 됩니다.
사실 SuspendThread / ResumeThread / TerminateThread 함수는
일반 프로그램에서 사용하라고 만들어 둔 것이 아니고 디버거에서 사용하는 기능입니다.
이들은 가급적 사용하지 않는 편이 좋습니다.
특히 쓰레드가 스스로 Suspend하는 일은 위험합니다.
Suspend와 Resume은 짝이 맞아야 합니다.
5번 Suspend하면 5번 Resume해야 합니다.
스스로 Suspend를 하도록 하는 경우
Suspend는 쓰레드 내에서 하지만
Resume은 다른 쓰레드에서 할 수 밖에 없습니다.
그런데 어떻게 하다 보면 Suspend 가 들어가기도 전인데
Resume이 먼저 실행되는 불행한 일이 발생합니다.
그 순간부터 이제 둘은 짝이 맞지 않게 되고 코드는 꼬이기 시작합니다.
스스로 Suspend로 들어가는 것은 "나 한가해요~"라는 의미일 텐데,
그냥 Sleep을 사용하는 편이 정신건강에 좋을 듯도 합니다.
Suspend의 또 다른 문제점은 멈추긴 멈췄는데,
뭘 하다 멈춘 건지 알 수가 없다는 것입니다.
굳이 알 필요가 없다면 관계없겠지만, 임계영역 내에서 멈출 수도 있습니다.
GetTickCount 함수로 현재 Tick을 가져와 뭘 하려는 순간 멈추면
Resume시에는 과거의 Tick으로 일을 하게 되는 것입니다.
이와 같이 여러 가지로 피곤한 일을 유발하기도 합니다.
그렇지만 이러한 단점들에도 Suspend가 매력적인 이유는
Sleep이 CPU의 시간을 쥐꼬리만큼 잡아먹는데 비해
Suspend는 CPU의 낭비가 전혀 없다는 점입니다.
시리얼 포트 TX 루틴을 쓰레드로 만든다고 했을 때,
Sleep을 사용하게 되면 버퍼에 데이터가 있는지 주기적으로 검사를 해야 합니다.
while not Terminated do begin if TxBufferIsEmpty then begin Sleep(10); end else begin .... end; end; |
여기서는 1초에 100번 버퍼에 보낼 데이터가 있는지 검사를 하고 있습니다.
CPU 입장에서는 대수로운 일이 아니지만 프로그래머 입장에서는 삽질처럼 보이기도 합니다.
이걸 Suspend로 고친다면,
TX버퍼가 비면 Suspend상태로 돌입, 메인쓰레드에서 TX버퍼로 데이터를 밀어 넣으면서 Resume,
Terminate시에 Resume. 말로 설계하기는 간단합니다만, 구현하기는 쉽지가 않습니다.
구현해 놓고 보더라도 프로그래머 입장에서는 엄청나게 CPU 리소스를 절약한 것 같지만,
CPU는 별로 좋아하는 것 같지도 않습니다. 그냥 코딩 하기 편하고 알아보기 쉬운 Sleep을 사용합시다.
그래도 쥐꼬리만큼의 낭비라도 줄이고 싶으시다면,
WaitForSingleObject를 사용하는 방법도 있습니다.
Timeout을 INFINITE로 주면 이벤트가 발생할 때까지 CPU 낭비가 없습니다.
TX버퍼로 데이터를 밀어 넣으면서 이벤트를 발생시키면 됩니다.
Wait 하기 전에 발생된 이벤트도 실종되는 일이 없으니 Suspend/Resume에 비해 사용하기가 쉽습니다.
Sleep vs. WaitForSingleObject
이 글을 쓰기 전에 혹시 뒷북 치는 건 아닌가 싶어,
게시판에 쓰레드 관련 강좌들을 둘러보았는데,
그러다가 지금까지는 전혀 생각해보지도 않았던 논제를 하나 발견 하게 되었습니다.
Sleep과 WaitForSingleObject, 둘 중 어느 놈이 더 좋은가 하는 문제였습니다.
대부분의 의견이 WaitForSingleObject가 낫다는 쪽이던데,
지금까지 제가 알고 있던 것과는 좀 달라서
두 API를 디버거로 따라가 보았습니다.
결론부터 말씀 드리면 원래의 함수 목적대로 사용하라는 겁니다.
껍데기는 다르지만 알맹이는 똑같습니다.
둘 다 시간지연에 ntdll!ZwDelayExecution을 사용하고 있습니다.
WaitForSingleObject는 거기에 더하여 이벤트를 기다리는 루틴을 추가하니,
순수한 딜레이 목적에는 Sleep을 사용하는 것이 보기에도 명확하고 API 본래 용도에 맞는 것입니다. WaitForSingleObject는 이름 그대로 이벤트를 기다리는 게 주목적이고
파라미터의 시간은 타임아웃을 위해 존재하는 것입니다.
게시물들 중에 Sleep은 딜레이 시간이 정확하지 않은데,
WaitForSingleObject는 딜레이가 정확하다는 의견이 있어 테스트 해 보았습니다.
테스트 결과 WaitForSingleObject도 정확하지 않습니다.
둘을 동시에 돌리면 둘이 똑같은 결과가 나옵니다.
테스트에 사용된 프로젝트 파일은 첨부에 달아 놓겠습니다.
WaitFor / OnTerminate
멤버 함수 중에 WaitFor라는 게 있습니다.
쓰레드가 끝날 때까지 대기하라는 함수로 쓰레드 내부에서 사용하는 것이 아니고
외부에서 사용하는 기능입니다.
게시판을 둘러보면 습관적으로 Terminate 뒤에 WaitFor를 사용하시는 분들도 있는데,
그럴 필요는 없습니다.
쓰레드가 완전히 끝나기를 기다렸다가 다른 작업을 시작해야 하는 경우에만 사용하면 됩니다.
예를 들어 프로그램 종료 시 쓰레드를 끝내고 메인 폼을 닫는다.
이런 경우 굳이 쓰레드를 기다릴 필요가 없습니다.
프로그램 종료 시 쓰레드를 끝내고 쓰레드의 작업 결과를 저장하고 폼을 닫는다.
이런 경우에 필요한 기능입니다.
OnTerminate라는 이벤트가 있습니다.
만약 쓰레드가 컴퍼넌트였다면 더블클릭으로 애용 될만한 이벤트인데,
코딩으로 엮어야 한다는 이유로 푸대접을 받고 있습니다.
이 이벤트는 쓰레드가 끝나기 직전에 쓰레드 내에서 호출을 하지만,
Synchronize에 싸여 호출하므로 결과적으로
메인쓰레드에서 실행되는 이벤트입니다.
위에 소개된 WaitFor를 사용해야 하는 경우,
이 이벤트를 대신 사용할 수도 있습니다.
WaitFor가 메인쓰레드를 얼어붙게 만드는데 비해
이 이벤트를 사용하면 그런 일이 없겠습니다.
'Delphi > 문법' 카테고리의 다른 글
델파이 제네릭(Generic) 용어 (0) | 2021.05.12 |
---|---|
델파이 제네릭(Generic) 개요 (0) | 2021.05.12 |
델파이 인터페이스 (0) | 2021.05.11 |
델파이 다중 동적 배열 (0) | 2021.05.11 |
델파이 예외처리 (0) | 2021.05.06 |
댓글