Command component (TCommand
) can be converted into asynchronous one using TAsyncCommand
class. Asynchronous means that all code implemented in DoExecute
method will be processed in a separate background thread. Today when each machine has access multiple CPU cores this functionality will allow to execute domain code in background, even in parallel, without any negative influence on displayed UI.
Introducing parallel programing into your project is not very simple in general, usually developers are struggling with many issues coming from that area, but in this days there is no other alternative and TAsyncCommand
pattern can make this transition much easier.
One of the simplest async commands can look like this code:
type
TSimpleAsyncCommand = class(TAsyncCommand)
protected
procedure DoExecute; override;
end;
procedure TSimpleAsyncCommand.DoExecute;
begin
DoSampleJobInBackgroundThread;
end;
The only difference between command executed in main thread and this one executed in background thread is base class TAsyncCommand
. This command launching looks the same like any other command:
TSimpleAsyncCommand.Create(aOwner).Execute;
This is very important to be sure which code on the async command is executed in background thread and which in main thread. Writing code working in background developer has very restricted access to outside "world". To force some portion of code to be executed in main thread you can use Synchronize method:
procedure TSimpleAsyncCommand.DoExecute;
begin
for i:=0 to fDataNames.Count-1 do
begin
LoadData (fDataNames[i]);
Synchronize(procedure begin
fReportMemo.Lines.Add('Step '+i.ToString
+', Data: '+fDataNames[i]);
end);
end;
end;
In this sample adding report line into TMemo component has to be done in main thread and data can be loaded in background thread.
Warning! Whereas using Synchronize looks like very simple solution it not recommended one. This should be used with full understanding that switching to main thread is very costly and during this time working thread (DoExecute code) is blocked, till the end of the Synchronize method.
Delphi gives developers a very easy method of testing background thread processing. Usually it's enough to set a breakpoint inside DoExecute method and verify processing flow and inspect a variable values. In more complex situations there could be needed to define thread name, by default TAsyncCommand background thread is named using following formula: 'TAsyncCommand - '+ClassName
, where ClassName is a name of this particular command class.
Method | Description |
---|---|
Execute |
Starts a new background thread and run DoExecute |
WithEventBeforeStart( aProc ) |
Provided method is called before DoExecute |
WithEventAfterFinish( aProc ) |
Provided method is called when DoExecute will finish |
WithEventOnProgress( aProc ) |
Provided method is called during command executing once a defined time (ProgressInterval ) |
Terminate |
Allows to break execution of background thread |
IsBusy: boolean |
Returns true when command was started and being processed |
IsTerminated: boolean |
Returns whether command processing should be terminated |
GetElapsedTime |
Returns time consumed by commands |
Events defined in methods: WithEventBeforeStart
, WithEventAfterFinish
and WithEventOnProgress
are processed in the main thread and can access all the VCL resources, but not directly background threads data and structures (this requires thread safe, critical section solution).
Event defined in WithEventOnProgress
method is called every defined interval (in milliseconds). The AsyncCommand is using an internal timer which is triggered with that interval. Then OnProgress event is executed in the main thread and if developer wants to access thread (command internal) data structures he has to use proper thread safe mechanism.
Property | Description |
---|---|
ProgressInterval: integer |
Defined interval of internal command timer (in milliseconds) which is calling OnProgress event. Default value = 100 ms |
Sample execution of TAsyncCommand:
cmdGenerateSampelCSV
.WithInjections([fCustomerID,fOrdersProxy])
.WithEventBeforeStart(
procedure
begin
aProgressBar.Position := 0;
end)
.WithEventOnUpdate(
procedure
begin
aProgressBar.Position := fMyAsyncCommand.GetProgressPercent;
end)
.WithEventAfterFinish(
procedure
begin
aProgressBar.Position := 100;
fCSVExporter.SaveToFile(aFileName, fMyAsyncCommand.ReportData);
aSeconds := cmdGenerateSampelCSV.GetElapsedTime.TotalSeconds;
LogAppPerformance(aReportName, aSeconds);
end)
.Execute;
- Remove code manipulating UI controls
- Remove as much of that code as it is possible
- The best approach is to remove all such code from
DoExecute
method TAsyncCommand
has a dedicated support for updating UI controls
- Use synchronize method if UI assess is required
- if assess to UI elements is required from background thread (
DoExecute
code) wrap such code accessing UI elements intoSynchronize
method - example bellow - Synchronize reduce a lot parallel processing capabilities and reduce a thread performance, therefore it is not the recommended solution
- if assess to UI elements is required from background thread (
- Do not share memory structures with main thread
- Use memory structures only internally (inside
DoExecute
) - for example if you want to access SQL server and fetch data it's better to create a new SQL connection component dedicated only for the async command
- Suggested solution is to: crate a structure colones before async execution, process everything using internal structures and get the results after processing
- Use memory structures only internally (inside
- Access shared memory structures inside critical section
- Use proper concurrency control structures like
TMonitor
to prevent parallel access to the same memory area by many threads
- Use proper concurrency control structures like
- Avoid memory sharing between multiple background threads
- Try to avoid such memory sharing because this is the most challenging scenario of parallel computing
- Proper solutions and patterns covering that scenario are far beyond the scope of this documentation