6
votes

I'm new to multithreading, but not a complete novice. I need to perform a call to a webservice in a worker thread.

In the main thread I have a form (TForm) with a private data member (private string) that only the worker thread will write to (I pass the a pointer to it into the thread before it resumes). When the worker thread has finished its webservice call and written the resultant response xml to the private member on the form, the worker thread uses PostMessage to send a message to the form's handle (which I also passed into the thread before it resumed).

interface

const WM_WEBSERVCALL_COMPLETE = WM_USER + 1;

type 
  TWebServiceResponseXML = string;
  PWebServiceResponseXML = ^TWebServiceResponseXML;

  TMyForm = class(TForm)
    ...
  private
    ...
    fWorkerThreadID: Cardinal;
    fWebServiceResponseXML: TWebServiceResponseXML;
  public
    ...
    procedure StartWorkerThread;
    procedure OnWebServiceCallComplete(var Message: TMessage); Message WM_WEBSERVCALL_COMPLETE;
  end;

  TMyThread = class(TThread)
  private      
  protected
    procedure Execute; override;
  public
    SenderHandle: HWnd;
    RequestXML: string;
    ResponseXML: string;
    IMyService: IService;
    PResponseXML: PWebServiceResponseXML;
  end;

implementation

procedure TMyForm.StartWorkerThread;
var
  MyWorkerThread: TMyThread;
begin
  MyWorkerThread := TMyThread.Create(True);
  MyWorkerThread.FreeOnTerminate := True;
  MyWorkerThread.SenderHandle := self.Handle;
  MyWorkerThread.RequestXML := ComposeRequestXML;
  MyWorkerThread.PResponseXML := ^fWebServiceResponseXML;
  MyWorkerThread.Resume;
end;

procedure TMyForm.OnWebServiceCallComplete(var Message: TMessage);
begin
  // Do what you want with the response xml string in fWebServiceResponseXML
end;

procedure TMyThread.Execute;
begin
  inherited;
  CoInitialize(nil);
  try
    IMyService := IService.GetMyService(URI);
    ResponseXML := IMyService.Search(RequestXML);
    PResponseXML := ResponseXML;
    PostMessage(SenderHandle, WM_WEBSERVCALL_COMPLETE, 0, 0);
  finally
    CoUninitialize;
  end;
end;

It works great, but now I want to do the same thing from a datamodule (which doesn't have a Handle)... so I would really appreciate some useful code to supplement the working model I have.

EDIT

What I really want is the code (if possible) that would allow me to replace the line

MyWorkerThread.SenderHandle := self.Handle;

with

MyWorkerThread.SenderHandle := GetHandleForThisSOAPDataModule;
3

3 Answers

9
votes

I have used this technique before with some success: Sending messages to non-windowed applications

Basically, use a second thread as a message pump on a handle obtained via AllocateHWND. This is admittedly irritating, and you would be better off using a library to handle all the details. I prefer OmniThreadLibrary but there are others - see How Do I Choose Between the Various Ways to do Threading in Delphi? and Delphi - Threading frameworks.

5
votes

You can allocate you own handle with AllocateHwnd and use that as a PostMessage target.

TTestThread = class(TThread)
private
  FSignalShutdown: boolean;
  // hidden window handle 
  FWinHandle: HWND;                       
protected
  procedure Execute; override;
  // our window procedure 
  procedure WndProc(var msg: TMessage);   
public
  constructor Create;
  destructor Destroy; override;
  procedure PrintMsg;
end;

constructor TTestThread.Create;
begin
  FSignalShutdown := False;

  // create the hidden window, store it's 
  // handle and change the default window 
  // procedure provided by Windows with our 
  // window procedure 

  FWinHandle := AllocateHWND(WndProc);
  inherited Create(False);
end;

destructor TTestThread.Destroy;
begin
  // destroy the hidden window and free up memory 
  DeallocateHWnd(FWinHandle);
  inherited;
end;

procedure TTestThread.WndProc(var msg: TMessage);
begin
  if Msg.Msg = WM_SHUTDOWN_THREADS then
    // if the message id is WM_SHUTDOWN_THREADS
    // do our own processing        
    FSignalShutdown := True 
  else        
    // for all other messages call 
    // the default window procedure 
    Msg.Result := DefWindowProc(FWinHandle, Msg.Msg, 
                                Msg.wParam, Msg.lParam);
end;

You can apply this to anything not just threads. Just beware that AllocateHWND is NOT threade safe as indicated here.

3
votes

Alternatives based on the use of an event:

  1. Use OnTerminate of the thread (already present) in combination with a flag:

    TMyDataModule = class(TDataModule)
    private    
      procedure OnWebServiceCallComplete(Sender: TObject);
    ...  
    
    TMyThread = class(TThread)
    public
      property TerminateFlag: Integer ...
    ...
    
    procedure TMyDataModule.StartWorkerThread;
      ...
      MyWorkerThread.OnTerminate := <Self.>OnWebServiceCallComplete;
      ...
    
    procedure TMyDataModule.OnWebServiceCallComplete(Sender: TObject);
    begin
      if MyWorkerThread.TerminateFlag = WEBCALL_COMPLETE then
        ...
    end;
    

    Set the TerminateFlag in the Execute routine. OnTerminate will automatically fire, even if FreeOnTerminate is True.

  2. Add a new event property to the thread class in which you may provide the flag as a parameter to indicate termination/thread result. Something like shown here. Be sure to synchronize the event call. Or forget the parameter and just only call the event if execution completed gracefully (like you're doing now).