How to get a result from an Anonymous Thread?

296 views Asked by At

Basically, what I need to do is this:

Show the user a "Please wait ..." form (lets call it waitForm) on top of the main form, execute http methods (get and post), and close the waitForm after I get the http response.

Since the http post method communicates with a physical device, it takes a while for the response to return (which is why I'm doing this in a worker thread, so the main thread doesn't appear to be "not responding").

I've set the http timeout to 1 minute. I've tried using an anonymous thread to execute the http methods so that the application doesn't go into "not responding", and so far its working as intended.

My problem here is that I need to use that response string further into the application after the thread is done. This is my code so far:

function TForm1.test: string;
var
  ReqStream, ResStream: TStringStream;
  http: TIdhttp;
  command, commandName: string;
begin
  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          waitForm.Label1.Caption := 'Please wait ...';
          waitForm.BitBtn1.Enabled := False;
          waitForm.FormStyle := fsStayOnTop;
          waitForm.Show;
        end);
      try
        http := TIdHTTP.Create(nil);
        ReqStream := TStringStream.Create('', TEncoding.UTF8);
        ResStream := TStringStream.Create('', TEncoding.UTF8);
        try
          command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
          commandName := 'sale';
          http.Request.ContentType := 'application/json';
          http.Request.CharSet := 'utf-8';
          http.Request.ContentEncoding := 'utf-8';
          http.Request.Accept := 'application/json';
          http.Request.Connection := 'keep-alive';
          http.ConnectTimeout := 60000;
          http.ReadTimeout := 60000;
          ReqStream.WriteString(command);
          ReqStream.Position := 0;
          try
            http.Post('http://localhost:5555/' + CommandName, ReqStream, ResStream);
            http.Disconnect;
            self.result := ResStream.DataString; // I know this can't be written like that
          except
            //
          end;
        finally
          FreeAndNil(ReqStream);
          FreeAndNil(http);
        end;
      finally
        TThread.Synchronize(nil,
        procedure
        begin
          waitForm.FormStyle := fsNormal;
          waitForm.BitBtn1.Enabled := True;
          waitForm.Close;
        end);
      end;
    end).Start;
ShowMessage(result); // this is where I call next procedure to parse the response.
end;

After we get a result from the test() function, I need to parse it and use it further in the application. fsStayOnTop and disabling the buttons is the only solution that I have found to discourage the user from interacting with the main form since .ShowModal is not an option because it blocks the function from continuing even with Synchronize() (am I wrong here?).

I've also tried using ITask and IFuture<string>, but I can't seem to make it work as intended. Maybe I should be using anonymous functions.

2

There are 2 answers

6
Remy Lebeau On BEST ANSWER

If you want TForm1.test() to work synchronously while using a worker thread internally, you will have to wait for that worker thread to finish before you can exit from your function. That will also allow your anonymous procedure to capture a local variable for it to write to, and then you can assign that variable to the function's Result after the thread is finished.

Try something more like this:

function TForm1.test: string;
var
  myThread: TThread;
  response: string;
begin
  waitForm.Label1.Caption := 'Please wait ...';
  waitForm.BitBtn1.Enabled := False;
  waitForm.FormStyle := fsStayOnTop;
  waitForm.Show;

  try
    myThread := TThread.CreateAnonymousThread(
      procedure
      var
        http: TIdHTTP;
        ReqStream: TStringStream;
        command, commandName: string;
      begin
        command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
        commandName := 'sale';
        http := TIdHTTP.Create(nil);
        try
          http.Request.ContentType := 'application/json';
          http.Request.CharSet := 'utf-8';
          http.Request.Accept := 'application/json';
          http.Request.Connection := 'close';
          http.ConnectTimeout := 60000;
          http.ReadTimeout := 60000;
          ReqStream := TStringStream.Create(command, TEncoding.UTF8);
          try
            response := http.Post('http://localhost:5555/' + CommandName, ReqStream);
          finally
            ReqStream.Free;
          end;
        finally
          http.Free;
        end;
      end
    );
    try
      myThread.FreeOnTerminate := False;
      myThread.Start;

      myThread.WaitFor;
      { alternatively...
      var H: array[0..1] of THandle;
      H[0] := myThread.Handle;
      H[1] := Classes.SyncEvent;
      repeat
        case MsgWaitForMultipleObjects(2, H, False, INFINITE, QS_ALLINPUT) of
          WAIT_OBJECT_0 + 0: Break;
          WAIT_OBJECT_0 + 1: CheckSynchronize;
          WAIT_OBJECT_0 + 2: Application.ProcessMessages;
          WAIT_FAILED      : RaiseLastOSError;
        end;
      until False;
      }
    finally
      if myThread.FatalException <> nil then
      begin
        //...
      end;
      myThread.Free;
    end;
  finally
    waitForm.FormStyle := fsNormal;
    waitForm.BitBtn1.Enabled := True;
    waitForm.Close;
  end;

  Result := response;
  ShowMessage(Result); // this is where I call next procedure to parse the response.
end

Otherwise, you should break up your logic to make TForm1.test() work asynchronously instead, and let it notify your code when the thread is finished and the response is available, eg:

procedure TForm1.do_test;
var
  myThread: TThread:
begin
  myThread := TThread.CreateAnonymousThread(
    procedure
    var
      http: TIdHTTP;
      ReqStream: TStringStream;
      command, commandName, response: string;
    begin
      command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
      commandName := 'sale';
      http := TIdHTTP.Create(nil);
      try
        http.Request.ContentType := 'application/json';
        http.Request.CharSet := 'utf-8';
        http.Request.Accept := 'application/json';
        http.Request.Connection := 'close';
        http.ConnectTimeout := 60000;
        http.ReadTimeout := 60000;
        ReqStream := TStringStream.Create(command, TEncoding.UTF8);
        try
          response := http.Post('http://localhost:5555/' + CommandName, ReqStream);
        finally
          ReqStream.Free;
        end;
      finally
        http.Free;
      end;
      TThread.Queue(nil,
        procedure
        begin
          Self.HandleResponse(response);
        end);
      end;
    end
  );
  myThread.OnTerminate := ThreadTerminated;

  waitForm.Label1.Caption := 'Please wait ...';
  waitForm.BitBtn1.Enabled := False;
  waitForm.FormStyle := fsStayOnTop;
  waitForm.Show;

  try
    myThread.Start;
  except
    ThreadTerminated(nil);
    raise;
  end;
end;

procedure TForm1.ThreadTerminated(Sender: TObject);
begin
  waitForm.FormStyle := fsNormal;
  waitForm.BitBtn1.Enabled := True;
  waitForm.Close;

  if (Sender <> nil) and (TThread(Sender).FatalException <> nil) then
  begin
    //...
  end;
end;

procedure TForm1.HandleResponse(const Response: string);
begin
  ShowMessage(Response); // this is where I call next procedure to parse the response.
end;

That being said...

I'm doing this in a worker thread, so the main thread doesn't appear to be "not responding"

While that is a good idea in general, I just want to point out that Indy does have a TIdAntiFreeze component to address this exact issue. You can leave your test() function to work synchronously without using a worker thread, letting TIdAntiFreeze pump the main message queue while TIdHTTP is blocking the main thread.

For example:

function TForm1.test: string;
var
  http: TIdHTTP;
  ReqStream: TStringStream;
  command, commandName: string;
begin
  command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
  commandName := 'sale';

  waitForm.Label1.Caption := 'Please wait ...';
  waitForm.BitBtn1.Enabled := False;
  waitForm.FormStyle := fsStayOnTop;
  waitForm.Show;

  // place a TIdAntiFreeze onto your Form, or
  // create it dynamically here, either way will work...

  try
    http := TIdHTTP.Create(nil);
    try
      http.Request.ContentType := 'application/json';
      http.Request.CharSet := 'utf-8';
      http.Request.Accept := 'application/json';
      http.Request.Connection := 'close';
      http.ConnectTimeout := 60000;
      http.ReadTimeout := 60000;
      ReqStream := TStringStream.Create(command, TEncoding.UTF8);
      try
        Result := http.Post('http://localhost:5555/' + CommandName, ReqStream);
      finally
        ReqStream.Free;
      end;
    finally
      http.Free;
    end;
  finally
    waitForm.FormStyle := fsNormal;
    waitForm.BitBtn1.Enabled := True;
    waitForm.Close;
  end;

  ShowMessage(Result); // this is where I call next procedure to parse the response.
end;
2
Marcodor On

You can retrieve the thread execution result in a callback:

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    Thread: TThread;
    ThreadResult: string;
    procedure OnThreadTerminate(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Waits;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  Thread := TThread.CreateAnonymousThread(procedure
  var HttpResult: string;
  begin
    TThread.Synchronize(nil, procedure
    begin
      WaitForm.Show;
    end);
    try
      Sleep(4000); // Here do Your http request
      // raise Exception.Create('Http request error');
      HttpResult := 'Result from thread';
    except
      HttpResult := 'Something bad happened';
    end;
    TThread.Synchronize(nil, procedure
    begin
      ThreadResult := HttpResult;
      WaitForm.Close;
    end);
  end);
  Thread.OnTerminate := OnThreadTerminate;
  Thread.Start;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Thread.Free;
end;

procedure TForm1.OnThreadTerminate(Sender: TObject);
begin
  Thread := nil;
  ShowMessage(ThreadResult);
end;

Note, there is no sense to put whole thread execution code in TThread.Synchronize. Synchronize execute the code in the main thread, so it will work like without thread.

This is just a proof of work, not sure I'd go with this design in a real scenario.