Delphi Thread Pool Example Using AsyncCalls

AsyncCalls Unit By Andreas Hausladen - Let's Use (and Extend) It!

Man using multiple screens to work on coding and programming.

hitesh0141 / Pixabay

This is my next test project to see what threading library for Delphi would suite me best for my "file scanning" task I would like to process in multiple threads / in a thread pool.

To repeat my goal: transform my sequential "file scanning" of 500-2000+ files from the non threaded approach to a threaded one. I should not have 500 threads running at one time, thus would like to use a thread pool. A thread pool is a queue-like class feeding a number of running threads with the next task from the queue.

The first (very basic) attempt was made by simply extending the TThread class and implementing the Execute method (my threaded string parser).

Since Delphi does not have a thread pool class implemented out of the box, in my second attempt I've tried using OmniThreadLibrary by Primoz Gabrijelcic.

OTL is fantastic, has zillion ways to run a task in a background, a way to go if you want to have "fire-and-forget" approach to handing threaded execution of pieces of your code.

AsyncCalls by Andreas Hausladen

Note: what follows would be more easy to follow if you first download the source code.

While exploring more ways to have some of my functions executed in a threaded manner I've decided to also try the "AsyncCalls.pas" unit developed by Andreas Hausladen. Andy's AsyncCalls – Asynchronous function calls unit is another library a Delphi developer can use to ease the pain of implementing threaded approach to executing some code.

From Andy's blog: 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. ... The AsyncCalls unit offers a variety of function prototypes to call asynchronous functions. ... It implements a thread pool! The installation is super easy: just use asynccalls from any of your units and you have instant access to things like "execute in a separate thread, synchronize main UI, wait until finished".

Beside the free to use (MPL license) AsyncCalls, Andy also frequently publishes his own fixes for the Delphi IDE like "Delphi Speed Up" and "DDevExtensions" I'm sure you've heard of (if not using already).

AsyncCalls In Action

In essence, all AsyncCall functions return an IAsyncCall interface that allows to synchronize the functions. IAsnycCall exposes the following methods:




 //v 2.98 of asynccalls.pas
IAsyncCall = interface
//waits until the function is finished and returns the return value
function Sync: Integer;
//returns True when the asynchron function is finished
function Finished: Boolean;
//returns the asynchron function's return value, when Finished is TRUE
function ReturnValue: Integer;
//tells AsyncCalls that the assigned function must not be executed in the current threa
procedure ForceDifferentThread;
end;

Here's an example call to a method expecting two integer parameters (returning an IAsyncCall):




 TAsyncCalls.Invoke(AsyncMethod, i, Random(500));




function TAsyncCallsForm.AsyncMethod(taskNr, sleepTime: integer): integer;
begin
result := sleepTime;

Sleep(sleepTime);

TAsyncCalls.VCLInvoke(
procedure
begin
Log(Format('done > nr: %d / tasks: %d / slept: %d', [tasknr, asyncHelper.TaskCount, sleepTime]));
end);
end;

The TAsyncCalls.VCLInvoke is a way to do synchronization with your main thread (application's main thread - your application user interface). VCLInvoke returns immediately. The anonymous method will be executed in the main thread. There's also VCLSync which returns when the anonymous method was called in the main thread.

Thread Pool in AsyncCalls

Back to my "file scanning" task: when feeding (in a for loop) the asynccalls thread pool with series of TAsyncCalls.Invoke() calls, the tasks will be added to internal the pool and will get executed "when time comes" (when previously added calls have finished).

Wait All IAsyncCalls To Finish

The AsyncMultiSync function defined in asnyccalls waits for the async calls (and other handles) to finish. There are a few overloaded ways to call AsyncMultiSync, and here's the simplest one:




function AsyncMultiSync(const List: array of IAsyncCall; WaitAll: Boolean = True; Milliseconds: Cardinal = INFINITE): Cardinal; 

If I want to have "wait all" implemented, I need to fill in an array of IAsyncCall and do AsyncMultiSync in slices of 61.

My AsnycCalls Helper

Here's a piece of the TAsyncCallsHelper:




WARNING: partial code! (full code available for download)
uses AsyncCalls;

type
TIAsyncCallArray = array of IAsyncCall;
TIAsyncCallArrays = array of TIAsyncCallArray;

TAsyncCallsHelper = class
private
fTasks : TIAsyncCallArrays;
property Tasks : TIAsyncCallArrays read fTasks;
public
procedure AddTask(const call : IAsyncCall);
procedure WaitAll;
end;




WARNING: partial code!
procedure TAsyncCallsHelper.WaitAll;
var
i : integer;
begin
for i := High(Tasks) downto Low(Tasks) do
begin
AsyncCalls.AsyncMultiSync(Tasks[i]);
end;
end;

This way I can "wait all" in chunks of 61 (MAXIMUM_ASYNC_WAIT_OBJECTS) - i.e. waiting for arrays of IAsyncCall.

With the above, my main code to feed the thread pool looks like:




procedure TAsyncCallsForm.btnAddTasksClick(Sender: TObject);
const
nrItems = 200;
var
i : integer;
begin
asyncHelper.MaxThreads := 2 * System.CPUCount;

ClearLog('starting');

for i := 1 to nrItems do
begin
asyncHelper.AddTask(TAsyncCalls.Invoke(AsyncMethod, i, Random(500)));
end;

Log('all in');

//wait all
//asyncHelper.WaitAll;

//or allow canceling all not started by clicking the "Cancel All" button:

while NOT asyncHelper.AllFinished do Application.ProcessMessages;

Log('finished');
end;

Cancel all? - Have To Change The AsyncCalls.pas :(

I would also like to have a way of "cancelling" those tasks that are in the pool but are waiting for their execution.

Unfortunately, the AsyncCalls.pas does not provide a simple way of canceling a task once it has been added to the thread pool. There's no IAsyncCall.Cancel or IAsyncCall.DontDoIfNotAlreadyExecuting or IAsyncCall.NeverMindMe.

For this to work I had to change the AsyncCalls.pas by trying to alter it as less as possible - so that when Andy releases a new version I only have to add a few lines to have my "Cancel task" idea working.

Here's what I did: I've added a "procedure Cancel" to the IAsyncCall. The Cancel procedure sets the "FCancelled" (added) field which gets checked when the pool is about to start executing the task. I needed to slightly alter the IAsyncCall.Finished (so that a call reports finished even when cancelled) and the TAsyncCall.InternExecuteAsyncCall procedure (not to execute the call if it has been cancelled).

You can use WinMerge to easily locate differences between Andy's original asynccall.pas and my altered version (included in the download).

You can download the full source code and explore.

Confession

NOTICE! :)





The CancelInvocation method stopps the AsyncCall from being invoked. If the AsyncCall is already processed, a call to CancelInvocation has no effect and the Canceled function will return False as the AsyncCall wasn't canceled.

The Canceled method returns True if the AsyncCall was canceled by CancelInvocation.

The Forget method unlinks the IAsyncCall interface from the internal AsyncCall. This means that if the last reference to the IAsyncCall interface is gone, the asynchronous call will be still executed. The interface's methods will throw an exception if called after calling Forget. The async function must not call into the main thread because it could be executed after the TThread.Synchronize/Queue mechanism was shut down by the RTL what can cause a dead lock.

Note, though, that you can still benefit from my AsyncCallsHelper if you need to wait for all async calls to finish with "asyncHelper.WaitAll"; or if you need to "CancelAll".

Format
mla apa chicago
Your Citation
Gajic, Zarko. "Delphi Thread Pool Example Using AsyncCalls." ThoughtCo, Aug. 28, 2020, thoughtco.com/delphi-thread-pool-example-using-asynccalls-1058157. Gajic, Zarko. (2020, August 28). Delphi Thread Pool Example Using AsyncCalls. Retrieved from https://www.thoughtco.com/delphi-thread-pool-example-using-asynccalls-1058157 Gajic, Zarko. "Delphi Thread Pool Example Using AsyncCalls." ThoughtCo. https://www.thoughtco.com/delphi-thread-pool-example-using-asynccalls-1058157 (accessed March 29, 2024).