Monday, October 21, 2019
Delphi Thread Pool Example Using AsyncCalls
Delphi Thread Pool Example Using AsyncCalls 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 Ive 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 Ive decided to also try the AsyncCalls.pas unit developed by Andreas Hausladen. Andys 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 Andys 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 Im sure youve heard of (if not using already). AsyncCalls In Action source code HTML 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 functions 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; Heres 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 (applications main thread - your application user interface). VCLInvoke returns immediately. The anonymous method will be executed in the main thread. Theres also VCLSync which returns when the anonymous method was called in the main thread. Thread Pool in AsyncCalls 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 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 heres the simplest one: function AsyncMultiSync(const List: array of IAsyncCall; WaitAll: Boolean True; Milliseconds: Cardinal INFINITE): Cardinal; dynamic array 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 two dimensional array Heres 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. Theres 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. Heres what I did: Ive 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 Andys original asynccall.pas and my altered version (included in the download). You can download the full source code and explore. Confession NOTICE! :) 2.99 version of AsyncCalls 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 wasnt 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 interfaces 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. no need to use my altered version 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.
Posted by w at 12:25 AM