AsyncCalls - asynchronous function calls

With AsyncCalls you can execute multiple functions at the same time and synchronize them at every point in the function or method that started them. This allows you to execute time consuming code whos result is needed at a later time in a different thread. While the asynchronous function is executed the caller function can do other tasks.

The AsyncCalls unit offers a variety of function prototypes to call asynchronous functions. There are functions that can call asynchron functions with one single parameter of the type: TObject, Integer, AnsiString, WideString, IInterface, Extended and Variant. Another function allows you to transfer a user defined value type (record) to the asynchron function where it can be modify. And there are functions that can call asynchron functions with a variable number of arguments. The arguments are specified in an const array of const and are automatically mapped to normal function arguments.

Inlined VCL/main thread synchronization is supported starting with version 2.0. With this you can implement the code that calls a VCL function directly in your thread method without having to use a helper method and TThread.Synchronize. You have full access to all local variables.
Version 2.9 introduces the TAsyncCalls class that utilizes generics and anonymous methods (Delphi 2009 or newer).

Download

AsyncCalls.zip Version 2.99 (34 KB)

License

The AsyncCalls unit is licensed under the Mozilla Public Licence ("MPL") version 1.1.

Installation

Extract the AsyncCalls.zip to a directory of your choice. Add the AnyncCalls.pas unit to your project and uses statements.

Requirements

Works with Delphi 5, 6, 7, 2005, BDS/Turbo 2006 and RAD Studio 2007, 2009, 2010, XE, XE2

Changelog

Example

procedure TForm1.Button3Click(Sender: TObject);
var
  Value: Integer;
begin
  TAsyncCalls.Invoke(procedure
  begin
    Value := 10;
    TAsyncCalls.VCLInvoke(procedure
    begin
      ShowMessage('The value may not equal 10: ' + IntToStr(Value));
    end);
    Value := 20;
    TAsyncCalls.VCLSync(procedure
    begin
      ShowMessage('The value equals 20: ' + IntToStr(Value));
    end);
    Value := 30;
  end);

  Sleep(1000);
end;

{ The cdecl function GetFiles() has two arguments, a string and an object which are declared like normal arguments. }
procedure GetFiles(const Directory: string; Filenames: TStrings); cdecl;
var
  h: THandle;
  FindData: TWin32FindData;
begin
  h := FindFirstFile(PChar(Directory + '\*.*'), FindData);
  if h <> INVALID_HANDLE_VALUE then
  begin
    repeat
      if (StrComp(FindData.cFileName, '.') <> 0) and (StrComp(FindData.cFileName, '..') <> 0) then
      begin
        Filenames.Add(Directory + '\' + FindData.cFileName);
        if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY <> 0 then
          GetFiles(Filenames[Filenames.Count - 1], Filenames);
      end;
    until not FindNextFile(h, FindData);
    Windows.FindClose(h);
  end;
end;

procedure TFormMain.ButtonGetFilesClick(Sender: TObject);
var
  Dir1, Dir2, Dir3: IAsyncCall;
  Dir1Files, Dir2Files, Dir3Files: TStrings;
begin
  Dir1Files := TStringList.Create;
  Dir2Files := TStringList.Create;
  Dir3Files := TStringList.Create;
  ButtonGetFiles.Enabled := False;
  try
    { Call the cdecl function GetFiles() with two arguments, a string and an object. }
    Dir1 := AsyncCall(@GetFiles, ['C:\Windows', Dir1Files]);
    { Call the cdecl function GetFiles() with two arguments, a string and an object. }
    Dir2 := AsyncCall(@GetFiles, ['D:\Html', Dir2Files]);
    { Call the cdecl function GetFiles() with two arguments, a string and an object. }
    Dir3 := AsyncCall(@GetFiles, ['E:', Dir3Files]);

    { Wait until both async functions have finished their work. While waiting make the UI reacting on user interaction. }
    while AsyncMultiSync([Dir1, Dir2], True, 10) = WAIT_TIMEOUT do
      Application.ProcessMessages;
    Dir3.Sync; // Force the Dir3 function to finish here

    MemoFiles.Lines.Assign(Dir1Files);
    MemoFiles.Lines.AddStrings(Dir2Files);
    MemoFiles.Lines.AddStrings(Dir3Files);
  finally
    ButtonGetFiles.Enabled := True;
    Dir3Files.Free;
    Dir2Files.Free;
    Dir1Files.Free;
  end;
end;

 


IAsyncCall interface

All AsyncCall functions return an IAsyncCall interface that allows to synchronize the functions.

  IAsyncCall = interface
    function Sync: Integer;
    function Finished: Boolean;
    function ReturnValue: Integer;
    function Canceled: Boolean;
    procedure ForceDifferentThread;
    procedure CancelInvocation;
    procedure Forget;
  end;

 

LocalAsyncCall function

LocalAsyncCall() executes the given local function/procedure in a separate thread. The result value of the asynchronous function is returned by IAsyncCall.Sync() and IAsyncCall.ReturnValue().
The LocalAsyncExec() function calls the IdleMsgMethod while the local procedure is executed.

function LocalAsyncCall(LocalProc: TLocalAsyncProc): IAsyncCall;
function LocalAsyncCallEx(LocalProc: TLocalAsyncProcEx; Param: INT_PTR): IAsyncCall;
procedure LocalAsyncExec(Proc: TLocalAsyncProc; IdleMsgMethod: TAsyncIdleMsgMethod);

Example

procedure MainProc(const S: string);
var
  Value: Integer;
  a: IAsyncCall;

  function DoSomething: Integer;
  begin
    if S = 'Abc' then
      Value := 1;
    Result := 0;
  end;

begin
  a := LocalAsyncCall(@DoSomething);
  // do something
  a.Sync;
  LocalAsyncExec(@DoSomething, Application.ProcessMessages);
end;

 

VCL synchronization

LocalVclCall() executes the given local function/procedure in the main thread. It uses the TThread.Synchronize function which blocks the current thread. LocalAsyncVclCall() execute the given local function/procedure in the main thread. It does not wait for the main thread to execute the function unless the current thread is the main thread. In that case it executes and waits for the specified function in the current thread like LocalVclCall().

The result value of the asynchronous function is returned by IAsyncCall.Sync() and IAsyncCall.ReturnValue().

procedure LocalVclCall(LocalProc: TLocalVclProc; Param: INT_PTR = 0);
function LocalAsyncVclCall(LocalProc: TLocalVclProc; Param: INT_PTR = 0): IAsyncCall;

Example

procedure TForm1.MainProc;

  procedure DoSomething;

    procedure UpdateProgressBar(Percentage: Integer);
    begin
      ProgressBar.Position := Percentage;
      Sleep(20); // This delay does not affect the time for the 0..100 loop
                 // because UpdateProgressBar is non-blocking.
    end;

    procedure Finished;
    begin
      ShowMessage('Finished');
    end;

  var
    I: Integer;
  begin
    for I := 0 to 100 do
    begin
      // Do some time consuming stuff
      Sleep(30);
      LocalAsyncVclCall(@UpdateProgressBar, I); // non-blocking
    end;
    LocalVclCall(@Finished); // blocking
  end;

var
  a: IAsyncCall;
begin
  a := LocalAsyncCall(@DoSomething);
  a.ForceDifferentThread; // Do not execute in the main thread because this will
                          // change LocalAyncVclCall into a blocking LocalVclCall
  // do something
  //a.Sync; The Compiler will call this for us in the Interface._Release method
end;

 

EnterMainThread/LeaveMainThread

EnterMainThread/LeaveMainThread can be used to temporary switch to the main thread. The code that should be synchonized (blocking) has to be put into a try/finally block and the LeaveMainThread() function must be called from the finally block. A missing try/finally will lead to an access violation.

procedure EnterMainThread;
procedure LeaveMainThread;

Example

procedure MyThreadProc;
var
 S: string;
begin
 Assert(GetCurrentThreadId <> MainThreadId);
 S := 'Hallo, I''m executed in the main thread';

 EnterMainThread;
 try
   Assert(GetCurrentThreadId = MainThreadId);
   ShowMessage(S);
 finally
   LeaveMainThread;
 end;

 Assert(GetCurrentThreadId <> MainThreadId);
end;

 

AsyncCall functions

The AsyncCall() functions start a specified asynchronous function.
The AsyncExec() function calls the IdleMsgMethod in a loop, while the async. method is executed.

function AsyncCall(Proc: TAsyncCallArgObjectProc; Arg: TObject): IAsyncCall; overload;
function AsyncCall(Proc: TAsyncCallArgIntegerProc; Arg: Integer): IAsyncCall; overload;
function AsyncCall(Proc: TAsyncCallArgStringProc; const Arg: string): IAsyncCall; overload;
function AsyncCall(Proc: TAsyncCallArgWideStringProc; const Arg: WideString): IAsyncCall; overload;
function AsyncCall(Proc: TAsyncCallArgInterfaceProc; const Arg: IInterface): IAsyncCall; overload;
function AsyncCall(Proc: TAsyncCallArgExtendedProc; const Arg: Extended): IAsyncCall; overload;
function AsyncCallVar(Proc: TAsyncCallArgVariantProc; const Arg: Variant): IAsyncCall; overload;

function AsyncCall(Method: TAsyncCallArgObjectMethod; Arg: TObject): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgIntegerMethod; Arg: Integer): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgStringMethod; const Arg: string): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgWideStringMethod; const Arg: WideString): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgInterfaceMethod; const Arg: IInterface): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgExtendedMethod; const Arg: Extended): IAsyncCall; overload;
function AsyncCallVar(Method: TAsyncCallArgVariantMethod; const Arg: Variant): IAsyncCall; overload;

function AsyncCall(Method: TAsyncCallArgObjectEvent; Arg: TObject): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgIntegerEvent; Arg: Integer): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgStringEvent; const Arg: string): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgWideStringEvent; const Arg: WideString): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgInterfaceEvent; const Arg: IInterface): IAsyncCall; overload;
function AsyncCall(Method: TAsyncCallArgExtendedEvent; const Arg: Extended): IAsyncCall; overload;
function AsyncCallVar(Method: TAsyncCallArgVariantEvent; const Arg: Variant): IAsyncCall; overload;

procedure AsyncExec(Method: TNotifyEvent; Arg: TObject; IdleMsgMethod: TAsyncIdleMsgMethod);

Example

function TestFunc(const Text: string): Integer;
begin
  Result := TimeConsumingFuncion(Text);
end;

a := AsyncCall(TestFunc, 'A Text');

 

AsyncCallEx functions

The AsyncCallEx() functions start a specified asynchronous function with a referenced value type (record) that can be manipulated in the asynchron function.

function AsyncCallEx(Proc: TAsyncCallArgRecordProc; var Arg{: TRecordType}): IAsyncCall; overload;
function AsyncCallEx(Method: TAsyncCallArgRecordMethod; var Arg{: TRecordType}): IAsyncCall; overload;
function AsyncCallEx(Method: TAsyncCallArgRecordEvent; var Arg{: TRecordType}): IAsyncCall; overload;

Example

type
  TData = record
    Value: Integer;
  end;

procedure TestRec(var Data: TData);
begin
  Data.Value := 70;
end;

a := AsyncCallEx(@TestRec, MyData);
{ Don't access "MyData" here until the async. function has finished. }
a.Sync; // MyData.Value is now 70

 

AsyncCall functions with a variable number of arguments

This AsyncCall() functions start a specified asynchronous function with a variable number of argument. The asynchron function must be declared as cdecl and the argument's modifier must be const for Variants. All other types can have the const modified but it is not necessary.

function AsyncCall(Proc: TCdeclFunc; const Args: array of const): IAsyncCall; overload;
function AsyncCall(Proc: TCdeclMethod; const Args: array of const): IAsyncCall; overload;

 

AsyncMultiSync

AsyncMultiSync() waits for the async calls and other handles to finish. MsgAsyncMultiSync() waits for the async calls, other handles and the message queue.

function AsyncMultiSync(const List: array of IAsyncCall; WaitAll: Boolean = True;
  Milliseconds: Cardinal = INFINITE): Cardinal;
function AsyncMultiSyncEx(const List: array of IAsyncCall; const Handles: array of THandle;
  WaitAll: Boolean = True; Milliseconds: Cardinal = INFINITE): Cardinal;
function MsgAsyncMultiSync(const List: array of IAsyncCall; WaitAll: Boolean;
  Milliseconds: Cardinal; dwWakeMask: DWORD): Cardinal;
function MsgAsyncMultiSyncEx(const List: array of IAsyncCall; const Handles: array of THandle;
  WaitAll: Boolean; Milliseconds: Cardinal; dwWakeMask: DWORD): Cardinal;

Arguments

List An array of IAsyncCall interfaces for which the function should wait.
Handles An array of THandle for which the function should wait.
WaitAll = True The function returns when all listed async calls have finished. If Milliseconds is INFINITE the async calls meight be executed in the current thread. The return value is zero when all async calls have finished. Otherwise it is WAIT_FAILED.
WaitAll = False The function returns when at least one of the async calls has finished. The return value is the list index of the first finished async call. If there was a timeout, the return value is WAIT_FAILED.
Milliseconds Specifies the number of milliseconds to wait until a timeout happens. The value INFINITE lets the function wait until all async calls have finished.
dwWakeMask see Windows.MsgWaitForMultipleObjects()

Limitations

Length(List)+Length(Handles) must not exceed MAXIMUM_ASYNC_WAIT_OBJECTS   (61 elements).

Return value

 


 

AsyncCalls Internals - Thread pool and waiting-queue

An execution request is added to the waiting-queue when an async. function is started. This request forces the thread pool to check if there is an idle/suspended thread that could do the job. If such a thread exists, it is reactivated/resumed. If no thread is available then it depends on the number of threads in the pool what happens. If the maximum thread number is already reached the request remains in the waiting-queue. Otherwise a new thread is added to the thread pool.

Threads that aren't idle/suspended take the oldest request from the waiting-queue an execute the associated async. function. If the waiting queue is empty the threads becomes idle/suspended.