Form1에서 WM_MYSHOW 메세지가 오면 'Hello World'를 찍도록 구성을 합니다.
const
WM_MYSHOW = WM_USER + 10;
TForm1 = class(TForm)
// ...
private
procedure WmMyShow(var Msg: TMessage); message WM_MYSHOW;
// ...
end;
procedure TForm1.WmMyShow(var Msg: TMessage);
begin
Memo1.Lines.Add('Hello world');
end;
MyThread라는 스레드 클래스를 만듭니다. 하는 일은 1초 대기하고 있다가 Form1에 WM_MYSHOW 메세지를 던져서 화면에 'Hello World'가 찍히도록 하는 겁니다.
TMyThread = class(TThread)
procedure Execute; override;
end;
procedure TMyThread.Execute;
begin
Sleep(1000);
SendMessage(Form1.Handle, WM_MYSHOW, 0, 0);
end;
이제 MyThread를 생성해서 사용을 해 보겠습니다. MyThread를 생성하고 2초 경과뒤에 스레드의 해제를 하도록 합니다.
[소스1]
procedure TForm1.Button1Click(Sender: TObject);
var
MyThread: TMyThread;
begin
MyThread := TMyThread.Create(false);
Sleep(2000);
MyThread.WaitFor; // <--- (1)
MyThread.Free;
end;
퀴즈 1. [소스 1]은 Deadlock이 될까요, 아니면 잘 넘어 갈까요?
상기 코드에서 (1)번 부분을 WaitFor 메소드를 사용하지 않고 WaitForSingleObject 라는 API로 변경해서 테스트를 해 보겠습니다.
[소스2]
procedure TForm1.Button2Click(Sender: TObject);
var
MyThread: TMyThread;
begin
MyThread := TMyThread.Create(false);
Sleep(2000);
WaitForSingleObject(MyThread.Handle, INFINITE); // <--- (2)
MyThread.Free;
end;
퀴즈 2. [소스 2]는 Deadlock이 될까요, 아니면 잘 넘어 갈까요?
(1)번과 (2)번의 수행 차이점을 파악을 했다고 하면 다음과 같은 사실을 할 수가 있습니다. 상기는 TThread.WaitFor 코드입니다(pseudo code).
function TThread.WaitFor: LongWord;
// ...
begin
// ...
if GetCurrentThreadID = MainThreadID then
begin
// 만약 WaitFor를 호출한 스레드가 메인 스레드라면, 메세지 처리를 하고 스레드의 종료를 기다린다.
PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE);
MsgWaitForMultipleObjects(2, H, False, 1000, QS_SENDMESSAGE);
end else
// 만약 WaitFor를 호출한 스레드가 메인 스레드가 아니라면, 메세지 처리 같은 것 없이 그냥 스레드의 종료를 기다린다.
WaitForSingleObject(H[0], INFINITE);
end;
여기에서 알 수가 있는 것은 SendMessage(혹은 Synchronize)에 의해서 메인 스레드와 User defined 스레드의 Deadlock을 막기 위해서 상기와 같이 WaitFor 내부에서 Window 메세지를 처리해 주는 것을 알 수가 있습니다. 델파이에서는 이러한 방식으로 메인 스레드와 기타 스레드간의 Deadlock을 막고 있습니다.
그런데, 만약 MyThread를 생성, 대기, 해제하는 코드가 메인 스레드(버튼 클릭 이벤트에서 코딩)가 아니고 또 다른 스레드에서 실행한면 어떻게 될까요?
[소스 3]
TWorkThread = class(TThread)
procedure Execute; override;
end;
procedure TWorkThread.Execute;
var
MyThread: TMyThread;
begin
MyThread := TMyThread.Create(false);
Sleep(2000);
MyThread.WaitFor; // <--- (3)
MyThread.Free;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
WorkThread: TWorkThread;
begin
WorkThread := TWorkThread.Create(false);
Sleep(2000);
WorkThread.WaitFor; // <--- (4)
WorkThread.Free;
end;
3, 4번도 결국은 마찬가지인듯..
Main 에서 WorkThread 의 Wait를 호출 하는데..
WorkThread 는 MyThread 의 Wait 를 호출 하니 결국은 Main Thread 가 MyThread 의 Wait 를 거는것과 "MainThread가 MyThread 를 대기하고있다" 라는 면에서는 본질적으로 차이가 없는..
| (3)번 | (4)번 | Deadlock or not |
A | MyThread.WaitFor | WorkThread.WaitFor | ? |
B | MyThread.WaitFor | WaitForSingleObject(WorkThread.Handle, INFINITE) | ? |
C | WaitForSingleObject(MyThread.Handle, INFINITE) | WorkThread.WaitFor | ? |
D | WaitForSingleObject(MyThread.Handle, INFINITE) | WaitForSingleObject(WorkThread.Handle, INFINITE) | ? |
개인적론 저런식으로 코딩해본적이 없는것 같네요!
그래서 TThread의 WaitFor 가 어떻게 구현되었는지.. 본적도 없구.. 사용해보지도 않았었던것 같습니다.
하지만 잘 모르는 사람은 충분히 저런 실수를 할 수도 있을것 같네요
GUI-Thread에서 Thread를 wait하고 , Thread에서 GUI-Thread 에 Syncronize하려고 할경우에 대한 주의를 해야겠구..
혹 불가피한 경우에 WaitFor를 이용하면 될 듯 하네요...(있을까 모르겠지만..)
신경을 끄라니? 왜 자꾸 이상한 얘기를 해?
스레드에서 메인 스레드(WinProc)와 동기화를 맞추기 위해 SendMessage나 PostMessage를 쓰지 않고 어떻게 동기화를 하라구?
상기 코드에서 SendMessage를 사용하지 않고 Synchronize를 사용해도 어차피 똑같은 현상이 발생해.
애시당초 Synchonize를 사용해서 예제를 보여 주려고 하다가 의미의 전달을 확실히 하기 위해 SendMessage를 사용한 것 뿐임.
[MainThread]
procedure TApplication.WndProc(var Message: TMessage);
begin
with Message do
// ...
case WM_NULL: CheckSynchronize;
// ...
end;
end;
function CheckSynchronize(Timeout: Integer = 0): Boolean;
begin
// ...
SyncProc.SyncRec.FMethod;
SetEvent(SyncProc.Signal);
// ...
end;
[Working Thread]
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord; QueueEvent: Boolean = False);
begin
// ...
WakeMainThread(SyncProcPtr.SyncRec.FThread);
WaitForSingleObject(SyncProcPtr.Signal, INFINITE);
// ...
end;
procedure TApplication.WakeMainThread(Sender: TObject);
begin
PostMessage(Handle, WM_NULL, 0, 0);
end;
PostMessage는 애초에 Lock이 걸리지 않으니 상관이 없고...
Synchronize 를 호출 하는 Thread 를 Main 에서 Wait 로 대기한다는 것은 애초에 그냥 Main 에서 처리해야 맞았다는 코드 아닌가요... 일시적으로 대기하고 타임아웃을 거는것도 아니고 무한히 기다릴거면...어차피 작업 끝날때까지 대기할거면 궂이 Thread 로 분리할 이유가 전혀 없었죠...
(만약 Notify가 필요 했던 거라면. 역시 PostMessage면 되구요)
제가 말하고 싶었던 것은 잘못짠 코드에 대한 회피법을 생각하는건 의미가 없다는 거였는데...
Synchronize 를 호출 하는 Thread 를 Main 에서 Wait 로 대기한다는 것은 애초에 그냥 Main 에서 처리해야 맞았다는 코드 아닌가요...
무슨 얘기를 하는지 모르겠네. 스레드에서 일어 나는 이벤트를 어떻게 메인 스레드만 가지고 처리를 하나?
(만약 Notify가 필요 했던 거라면. 역시 PostMessage면 되구요)
PostMessage로 처리되지 않는, 반드시 SendMessage를 사용해야만 하는 경우가 수두룩해.
class TCPClient
{
string msg;
};
procedure TCPClientThread.Execute;
begin
while not Terminated do
begin
tcpClient.msg := read_from_socket(...);
if tcpClient.msg = '' then break;
ASynchronize(ShowMsg); // only PostMessage
end;
tcpClient.msg := '연결이 끊겼습니다';ASynchronize(ShowMsg); // only PostMessage
end;
이 코드에서 TCP 데이터 수신이 오자 마자 연결이 끊기는 경우 마지막 수신한 데이터를 화면에 보여 주지 못하는 경우가 발생할 수도 있어. thread에서 msg 버퍼를 실시간으로 생성하고 ShowMsg에서 해제시켜 주면 모를까, 이건 굉장히 귀찮은 작업이자나. 또 PostMessage만을 사용하는 경우 tcpClient 객체 인스턴스가 해제된 이후에 해당 코드(ShowMsg)가 불려 져서 Access Violation Error가 날 수도 있고. 결국 이를 위해서 동기화 구조를 써야 해. 델파이에서 사용하는 (PostMessage + Event 대기) 도 결국 SendMessage와 다를 바가 없는 거구.
Thread에서 이벤트가 발생한다는건 Main Thread 와 비동기적으로 처리하기 위한건데 .. Main 에서 이벤트 발생하는 Thread를 Wait 한다는것은 사실상 동기적인 코드라고 볼 수 있는 것 아닌가요... Thread에서 계산 결과 같은것이 SendMessage로 나올 거라면 Wait 후에 Thread 객체가 가진 결과를 받아서 쓰면 될 거고.. 메시제 날릴 필요 없이. UI 등을 Update ,참조 할거면 MainThread에서 반드시 작업을 해야하니 SendMessage를 사용 하겠지만, 그걸 MainThread 가 Wait 하고 있을 이유는 없구요.
마지막 코드같은경우도 솔직히 alloc -> post -> free 를 하거나, 공유되는 패킷 큐를 소유하는편이 맞다고 보는데..
아니 궂이 그렇지 않더라도... SendMessage는 물론 할 수 있지만 그걸 Main에서 Wait 하는건 애초에 데드락 걸라고 작정하지 않은이상 만들리 없는 코드라고 봄
Button1Click에서 스레드의 생성&실행과 대기&해제 코드를 같이 넣다 보니 의미의 전달이 제대로 되지 않는 것 같군.
다음과 같이 설명하면 되려나? 아래 코드에서 (1)을 WaitForSingeObject로 바꾸면 Deadlock이 걸린다는 게 본 글의 요지임.
var
MyThread: TMyThread;
procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread := TMyThread.Create(false);
end;
// Button1을 누르고 0.5초 이후에 클릭(MyThread.Execute 실행이 종료되기 이전)
procedure TForm1.Button2Click(Sender: TObject);
begin
MyThread.WaitFor; // <--- (1)
MyThread.Free;
end;
마지막 코드같은경우도 솔직히 alloc -> post -> free 를 하거나, 공유되는 패킷 큐를 소유하는편이 맞다고 보는데..
이 경우 PostMesssage를 통해서 Message를 받아 처리하는 Form1상의 코딩이 있다고 가정을 해.
procedure TForm1.MyMessage(var Msg: TMessage);
begin
// --- (A)
end;
Only PostMesssage 방식이라면 Lyn군의 얘기대로 하면 작업 스레드에서 alloc > PostMessage > 메인 스레드에서 free 하면 buf의 문제점은 해결이 되지. 하지만 (A)에서는 tcpClient 객체를 절대 건드리면 안된다는 가정이 생겨. 왜냐? tcpClient 뿐 아니라 관련된 스레드까지 가 종료되고 해제까지 된 이후에 본 코드가 실행이될 수도 있거든. 그렇게 따지면 프로그램 작성에 굉장히 큰 제약이 생길 수 밖에 없어.
그렇기 때문에 동기화(Synchronize)를 위해서 델파이 예전 버전에서는 SendMessage를 사용하였고, 지금 델파이 버전에서는 PostMessage + Event를 사용해서 동기화 코드(MyMessage)의 실행이 완료될때까지 기다리는 거야. 델파이 버전이 업글되면서 SendMessage 대신에 PostMessage + Event를 사용한 것은 Linux와 버전과 코드상의 호환을 맞추기 위해서이지, 결국은 SendMessage와 다름이 없는 얘기이고.
그리고 Lyn이 얘기한 공유하는 패킷 큐를 관리하는 녀석은 누가 관리해야 하지? 혹시라도 그런 공유 객체까지 해제가 된 뒤에 PostMessage에 의해서 상기 MeMessage 코드가 실행이 된다면 어떻할래? 그것을 막기 위해 객체간의 해제 시점을 고려하는 것보다가는 그냥 Synchronize 자체에서서 해당 코드(MyMessage)의 실행의 완료를 기다리는 것이 더 현명해.
코딩 스타일의 문제일까요... 전 소켓프로그램에서 UI가 살아 있는상태(즉 A가 실행 가능한)에서 tcpclient 가 메모리에서 해제 될 가능성이 있다는 것 자체가 발생할 수 없다고 생각하는데..
역시 소켓프로그램에서 프로그램이 죽기 전에 패킷 큐가 해제된다는것도...
근데 지금 제가 이야기 하고 싶은건 SendMessage를 쓰는 것 자체가 문제가 아니라...
그럴 가능성이 있는 Thread를 Wait 하는것이 문제라는건데... 제 머리속에선 이건 데드락 걸라고 작정한 코드라고 보이는..
전 소켓프로그램에서 UI가 살아 있는상태(즉 A가 실행 가능한)에서 tcpclient 가 메모리에서 해제 될 가능성이 있다는 것 자체가 발생할 수 없다고 생각하는데..
PostMessage를 사용하면 그런 경우가 발생한다는 것을 얘기하는 거야. 보통의 경우 Form위에 IdTCPClient 컴포넌트를 올려 놓고 사용하니까 문제가 되지 않지만, 실시간으로 필요가 있을 때 객체를 생성하고 필요가 없을 때 객체 해제를 하게 되면 MyMessage에서 IdTCPClient 객체에 Access하게 되는 경우 에러가 발생해.
var
IdTCPClient: TIdTCPClient;
procedure TForm1.btnOpenClick(Sender: TObject);
begin
// IdTCPClient 생성
// IdTCPClient 연결
// TCPClientThread 생성 및 실행
end;
procedure TForm1.btnCloseClick(Sender: TObject);
begin
// IdTCPClient 연결 끊기
// TCPClientThread 객체 파괴
// IdTCPClient 객체 파괴
end;
그럴 가능성이 있는 Thread를 Wait 하는것이 문제라는건데... 제 머리속에선 이건 데드락 걸라고 작정한 코드라고 보이는..
뭐가 Deadlock의 가능성이 있는데?
Sleep(1000)이?
Execute 마지막 부분에 Synchronize(SendMessage)가?
아니면 메인 Thread에서 작업 Thread를 종료하려 할 때 Wait하는 것이?
TCP 프로그래밍할 때 TCP Data 수신 스레드에서 소켓 연결이 끊기는 경우 Thread Body(Execute) 마지막 부분에 "연결이 끊겼습니다" 보여 주기 위해 Synchronize를 사용하는 것이 보통 아닌가? 그리고 Thread개 해제할 때(Thread.Free) 내부적으로 Wait 호출하지 않을 것 같아?
정리를 해 보겠습니다.
A. Thread를 해제할 때 Thread Body(Execute)의 실행 완료를 기다립니다. 당연한 것이 Thread가 해제되고 난 이후에 관련된 객체의 접근을 방지하기 위해서입니다. 굳이 프로그래머가 Thread.WaitFor를 호츨하지 않아도 내부적으로는 WaitFor를 호출하고 있습니다. TThread.Destroy 코드를 보면 이를 확인할 수가 있습니다.
[Classes.pas]
destructor TThread.Destroy;
begin
if (FThreadID <> 0) and not FFinished then
begin
Terminate;
if FCreateSuspended then
Resume;
WaitFor;
end;
// ...
end;
B. 작업 스레드에서 메인 스레드와 동기화를 위해서는 Block이되는 API를 사용해야 합니다. Win32 기반으로 코딩을 할 때에 Thread Body에서는 SendMessage를 사용하고, 델파이의 TThread에서는 Synchronize를 사용합니다. 그런데 이 Synchronize도 결국 SendMessage와 다를게 없습니다(PostMessage + Event.Wait). 비동기식으로 처리(PostMessage)하게 되면 여러가지 문제점이 발생할 수 있기 때문에, 델파이에서도 Synchronize를 위해 SendMessage(혹은 PostMesage + Event.Wait)를 사용하는 것입니다.
다음과 같은 구조를 봅시다.
[보기 1]
Main Thread > Thread(1) > Thread(2) > Thread(3) > Thread(3) > ... Thread(n) -> Main Thread
ButtonClick 이벤트에 의해서 Main Thread는 Thread(1)의 종료를 기다리고, 있고 Thread(1)는 Thread(2)의 종료를 기다리고 있고, ... 그리고 마지막 Thread(n)에서는 Synchronize를 사용해서 Main Thread에서의 메세지 처리를 기다리고 있다고 가정하겠습니다.
A, B에 근거하여 보면 [보기 1]은 Thread hang이 걸리는 구조(Deadlock)로 볼 수가 있죠. Deadlock은 이런 연결 구조에서 하나만 끊기면 해결이 됩니다. 그렇다면 어디를 끊어야 할까요?
그러기 위해서 바로, MainThread의 Thread(1) 종료 대기 루틴에서 PeekMessage를 사용해야 한다는 것입니다. 메인 스레드에서 PeekMessag를 사용하게 되면 수신되는 메세지를 처리를 차례대로 수행하게 되고 자연스럽게 Thread(n)과 Main Thread간의 연결 고리가 제일 먼저 끊어 집니다. 그 다음부터는 모든 스레드의 연결 고리가 차례대로 풀리게 되죠(화살표 반대 방향 순서로). 이를 위해서 Main Thread에서 특정 Thread의 종료를 대가하는 경우에는 반드시 스레드의 종료를 대기함과 동시에 메세지 처리를 해야 하고, 그것때문에 델파이의 WaitFor라는 메소드 안에서 WaitFor 호출을 한 스레드가 Main Thread인 경우 PeekMessage를 하는 것입니다.
[결론]
1. 작업 스레드에서 메인 스레드와 동기화를 맞추기 위해서는 PostMessage보다 Block이 되는 SendMessage를 사용하는 것이 좋다.
2. 스레드를 해제해야 하는 경우 반드시 Thread Body의 실행을 완료할 때까지 기다리는 것이 좋다.
3. 단 2번에서, WaitFor를 호출하는 놈이 메인 스레드이면 반드시 자신에게 들어 오는 메세지 처리를 해 줘야 Deadlock이 걸리지 않는다.
델파이의 TThread 클래스는 이것을 모두 내부에서 처리를 해 주기 때문에 그냥 대충 이용만 해도(알 필요도 없고) Deadlock이 잘 발생하지 않는다.
뭐 좋은 글이긴 한데...
애초에 MainThread 로 SendMessage를 한다는 것 부터가 문제인거같단...(것도 UI건드리는데 ㅡ,.ㅡ)