delphitthread

What is the proper way to wait for TThread instance to start up


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.


Solution

  • 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;