Search code examples
multithreadinghttpdelphitthread

How to get a result from an Anonymous Thread?


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.


Solution

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