Between time of TThread
instance creation and start up, main thread will continue with code execution. If code in main thread depends on thread in question to be fully up and running it has to wait somehow until thread Execute
method actually started.
Consider following code:
const
WM_MY_ACTION = WM_APP + 10;
type
TWndThread = class(TThread)
protected
fWndHandle: THandle;
IsRunning: boolean;
procedure WndProc(var Msg: TMessage);
procedure Execute; override;
public
Test: integer;
procedure AfterConstruction; override;
procedure DoAction;
end;
procedure TWndThread.AfterConstruction;
begin
inherited;
while not IsRunning do Sleep(100); // wait for thread start up
end;
procedure TWndThread.Execute;
var
Msg: TMsg;
begin
fWndHandle := AllocateHWnd(WndProc);
IsRunning := true;
try
while not Terminated do
begin
if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
begin
while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end;
end;
finally
DeallocateHWnd(fWndHandle);
end;
end;
procedure TWndThread.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_MY_ACTION:
begin
inc(Test);
end;
else Msg.Result := DefWindowProc(fWndHandle, Msg.Msg, Msg.WParam, Msg.LParam);
end;
end;
procedure TWndThread.DoAction;
begin
PostMessage(fWndHandle, WM_MY_ACTION, 0, 0);
end;
var
t: TWndThread;
begin
t := TWndThread.Create;
t.DoAction;
t.Terminate;
end;
Without loop that waits for IsRunning
flag, DoAction
will not be able to successfully post message to contained window handle because it will not yet be created. Basically, inc(Test)
inside WndProc
will not be triggered.
Is there a better way to wait for thread start up and complete necessary initialization inside Execute
method or is this solution as good as it gets?
Note: I am aware that AllocateHWnd
and DeallocateHWnd
are not thread safe and should not be used in production code like above example.
Change FIsRunning
from Boolean
to TEvent
to get signaled if everything is ready to use.
Now you can wait for this event at any point (especially in the public methods like DoAction
):
const
WM_MY_ACTION = WM_APP + 10;
type
TWndThread = class(TThread)
private
FIsRunning: TEvent; // <- event
protected
fWndHandle: THandle;
procedure WndProc(var Msg: TMessage);
procedure Execute; override;
procedure CheckIsRunning; // guard method
public
constructor Create;
destructor Destroy; override;
procedure DoAction;
end;
constructor TWndThread.Create;
begin
// create event
FIsRunning := TEvent.Create( nil, True, False, '' );
inherited;
end;
destructor Destroy;
begin
inherited;
// free event
FIsRunning.Free;
end;
procedure CheckIsRunning;
begin
// guard if terminated
if Terminated then
raise Exception.Create( 'Already terminated' );
// wait for event
FIsRunning.WaitFor();
end;
procedure TWndThread.Execute;
var
Msg: TMsg;
begin
fWndHandle := AllocateHWnd(WndProc);
// set event
FIsRunning.SetEvent;
try
while not Terminated do
begin
if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
begin
while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end;
end;
finally
DeallocateHWnd(fWndHandle);
end;
end;
procedure TWndThread.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_MY_ACTION:
begin
inc(Test);
end;
else Msg.Result := DefWindowProc(fWndHandle, Msg.Msg, Msg.WParam, Msg.LParam);
end;
end;
procedure TWndThread.DoAction;
begin
// guard method
CheckIsRunning;
// do the action
PostMessage(fWndHandle, WM_MY_ACTION, 0, 0);
end;
Now everything is very easy to use and you only have to wait, if there is a special reason for waiting (accessing the DoAction
method too fast)
var
t: TWndThread;
begin
t := TWndThread.Create;
try
t.DoAction;
finally
t.Free;
end;
end;