这是我的下一个测试项目,看看Delphi线程库最适合我的“文件扫描”任务,我想在多线程/线程池中处理。...
这是我的下一个测试项目,看看Delphi线程库最适合我的“文件扫描”任务,我想在多线程/线程池中处理。
重复我的目标:将500-2000多个文件的顺序“文件扫描”从非线程方法转换为线程方法。我不应该一次运行500个线程,因此我想使用一个线程池。线程池是一个类似队列的类,它为许多正在运行的线程提供队列中的下一个任务。
第一次(非常基本的)尝试是通过简单地扩展TThread类并实现Execute方法(我的线程字符串解析器)来实现的。
由于Delphi没有现成实现的线程池类,在我的第二次尝试中,我尝试使用Primoz Gabrijelcic的OmniThreadLibrary。
OTL非常棒,它有无数种在后台运行任务的方法,如果您想使用“启动并忘记”的方法来处理代码片段的线程执行,那么这是一种可行的方法。
andreas Hausladden的异步调用
Note: what follows would be more easy to follow if you first download the source code.
在探索以线程方式执行某些函数的更多方法的同时,我还决定尝试Andreas Hausladden开发的“AsyncCalls.pas”单元。Andy的AsyncCalls–Asynchronous function calls unit是另一个库,Delphi开发人员可以使用它来减轻实现线程化方法来执行某些代码的痛苦。
来自Andy的博客:使用AsyncCalls,您可以同时执行多个函数,并在启动它们的函数或方法中的每个点同步它们。。。AsyncCalls单元提供多种函数原型来调用异步函数。。。它实现了一个线程池!安装非常简单:只需使用来自任何单元的异步调用,就可以立即访问“在单独的线程中执行,同步主UI,等待完成”之类的内容。
除了免费使用(MPL许可证)异步调用之外,Andy还经常发布他自己对Delphi IDE的修复,如“Delphi加速”和“DDevExtensions”,我相信您一定听说过(如果还没有使用)。
异步调用正在运行
本质上,所有AsyncCall函数都返回一个IAsyncCall接口,该接口允许同步函数。IASNycall公开了以下方法:
//
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".