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 ActionWhile there's only one unit to include in your application, the asynccalls.pas provides more ways one can execute a function in a different thread and do thread synchronization. Take a look at the source code and the included HTML help file to get familiar with the basics of asynccalls.
In essence, all AsyncCall functions return an IAsyncCall interface that allows to synchronize the functions. IAsnycCall exposes the following methods:
As I fancy generics and anonymous methods I'm happy that there's a TAsyncCalls class nicely wrapping calls to my functions I want to be executed in a threaded manner.
//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
Here's an example call to a method expecting two integer parameters (returning an IAsyncCall):
The AsyncMethod is a method of a class instance (for example: a public method of a form), and is implemented as:
Again, I'm using the Sleep procedure to mimic some workload to be done in my function executed in a separate thread.
function TAsyncCallsForm.AsyncMethod(taskNr, sleepTime: integer): integer;
result := sleepTime;
Log(Format('done > nr: %d / tasks: %d / slept: %d', [tasknr, asyncHelper.TaskCount, sleepTime]));
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 AsyncCallsAs explained in the examples/help document (AsyncCalls Internals - Thread pool and waiting-queue): An execution request is added to the waiting-queue when an async. function is started...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.
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 FinishI needed a way to execute 2000+ tasks (scan 2000+ files) using TAsyncCalls.Invoke() calls and also to have a way to "WaitAll".
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:
There's also one limitation: Length(List) must not exceed MAXIMUM_ASYNC_WAIT_OBJECTS (61 elements). Note that List is a dynamic array of IAsyncCall interfaces for which the function should wait.
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 HelperTo help myself implementing the WaitAll method, I've coded a simple TAsyncCallsHelper class. The TAsyncCallsHelper exposes a procedure AddTask(const call : IAsyncCall); and fills in an internal array of array of IAsyncCall. This is a two dimensional array where each item holds 61 elements of IAsyncCall.
Here's a piece of the TAsyncCallsHelper:
And the piece of the implementation section:
WARNING: partial code! (full code available for download)
TIAsyncCallArray = array of IAsyncCall;
TIAsyncCallArrays = array of TIAsyncCallArray;
TAsyncCallsHelper = class
fTasks : TIAsyncCallArrays;
property Tasks : TIAsyncCallArrays read fTasks;
procedure AddTask(const call : IAsyncCall);
Note that Tasks[i] is an array of IAsyncCall.
WARNING: partial code!
i : integer;
for i := High(Tasks) downto Low(Tasks) do
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:
Again, Log() and ClearLog() are two simple function to provide visual feedback in a Memo control.
procedure TAsyncCallsForm.btnAddTasksClick(Sender: TObject);
nrItems = 200;
i : integer;
asyncHelper.MaxThreads := 2 * System.CPUCount;
for i := 1 to nrItems do
Cancel all? - Have To Change The AsyncCalls.pas :(Since I have 2000+ tasks to be done, and the thread poll will run up to 2 * System.CPUCount threads - tasks will be waiting in the tread pool queue to be executed.
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.
ConfessionI've altered the asynccalls.pas in a way that it suits my specific project needs. If you do not need "CancelAll" or "WaitAll" implemented in a way described above, be sure to always, and only, use the original version of asynccalls.pas as released by Andreas. I am hoping, though, that Andreas will include my changes as standard features - maybe I'm not the only developer trying to use AsyncCalls but just missing a few handy methods :)
NOTICE! :)Just a few days after I wrote this article Andreas did release a new 2.99 version of AsyncCalls. The IAsyncCall interface now includes three more methods:
Therefore, no need to use my altered version.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".