• Blog
  • How to take advantage of the Delphi multithreading classes

How to take advantage of the Delphi multithreading classes

How to use Delphi’s parallel programming libraries in a scenario closer to reality

Publish date:
Discover more of what matters to you

Overview

Delphi provides a set of libraries to work with multithreading features starting from the version XE7, however, considering that they are relatively fresh, many developers forget about their existence, or think that it’s too complicated to use them.

The purpose of this article is to demonstrate a scenario in which the use of multithreading can result in a better app response time.

Parallel Programming Library

PPL (Parallel Programming Library) provides a mechanism to distribute complex or long routines in parallel threads, running them at the same time. So, since they run in parallel execution flows, the time of completion of the whole routine is clearly less than in the cases when we use a “traditional” approach (calling the methods of the routine in sequence, that is, in a “linear” way).

The image below shows the difference between a single thread and multiple threads (multithreading) in the same process:

The difference between a single thread and multiple threads (multithreading) in the same process
The difference between a single thread and multiple threads (multithreading) in the same process

Basically, if you split the main process into three small parts, and run all of them simultaneously, the duration of the method would be three times faster, depending on the occasion.

How to use Delphi’s parallel programming libraries in a scenario closer to reality?

For instance, let’s take sales software that is used for printing an order report with the following sections:

  • Customer data (name, address, document…)
  • Order data (date, total value, payment method…)
  • Order items details (product, description, quantity…)

This functionality requires the software to pull the information from the database for these three sections in separate queries. Let’s say that each query takes the following time indicated below:

  • Customer data: 2 seconds
  • Order data: 3 seconds
  • Order items details: 4 seconds

We could write the printing method code in the usual way:

123
GetCustomerData;
GetOrderData;
GetOrderItemsData;

Also, to simulate the time that each method takes to get the required data, I added a Sleep procedure call with their respective duration:

1234567891011121314
procedure GetCustomerData;
begin
Sleep(2000);
end;
procedure GetOrderData;
begin
Sleep(3000);
end;
procedure GetOrderItemsData;
begin
Sleep(4000);
end;

Next, we can add a TStopWatch variable to calculate the queries’ duration, and then display it as a message at the end of the method:

123456789101112131415
procedure PrintReport;
var
StopWatch: TStopWatch;
begin
StopWatch:= TStopWatch.StartNew;
GetCustomerData;
GetOrderData;
GetOrderItemsData;
StopWatch.Stop;
ShowMessage(Format('Queries completed in %s seconds.',
[StopWatch.Elapsed.Seconds.ToString]));
end;

Running the application and calling the method, we will get this message:

Multithreading

Now we would like to show you that we can improve this performance. By using parallel programming, we can distribute each query in an isolated thread and run them all simultaneously. That’s when Delphi’s PPL enters the game. This improvement can be achieved with the TTask class from the System.Threading namespace the following way:

1234567891011
uses
System.Threading;
{ … }
var
Task: ITask;
begin
Task := TTask.Create({method});
Task.Start;
end;

But we can’t simply create a task for each query. There is a condition that should catch our attention. You have probably noticed that each query has a different duration, right? It means that, if we run the three queries in different tasks, it’s highly likely that the longest queries will not be finished in time. In our scenario, the report would be printed in two seconds, which is the shortest query, but the other sections (order and items data) would not be displayed in the report, because their queries would not be finished.

We can resolve this situation by creating an array of the ITask interface and assigning each query to a position of the array. It will allow us to use a method named WaitForAll to make sure that the main thread continues only when all threads are finished. Check it out:

1234567891011121314151617181920212223
procedure PrintReport;
var
Tasks: array [0..2] of ITask;
StopWatch: TStopWatch;
begin
StopWatch := TStopWatch.StartNew;
Tasks[0] := TTask.Create(GetCustomerData);
Tasks[0].Start;
Tasks[1] := TTask.Create(GetOrderData);
Tasks[1].Start;
Tasks[2] := TTask.Create(GetOrderItemsData);
Tasks[2].Start;
TTask.WaitForAll(Tasks);
StopWatch.Stop;
ShowMessage(Format('Queries completed in %s seconds.',
[StopWatch.Elapsed.Seconds.ToString]));
end;

Now, if we run the application again and call the printing method, we will get the following result:

multithreading

As we can verify, with multithreading, we could reduce the report printing duration to half of the time. 

What about one more quick example? Pay attention that this one will be different.

Let’s consider software that has the following batch file reading routine:

12345678910111213
procedure TForm1.ReadBatchFiles;
var
FileList: TStringList;
i: integer;
begin
FileList := TStringList.Create;
FileList.Text := GetFilesToRead;
for i := 0 to Pred(FileList.Count) do
ReadFile(FileList[i]);
FileList.Free;
end;

This operation can take really long depending on the number of files and their sizes. As we know, the application will probably stop responding while this method is running, because the loop will be performed in the main thread. It will result in the “freezing” effect, as if the application crashed, causing the user to end the application through the Windows task manager in the middle of the process.

The solution is to move this operation to a thread. We know that we can inherit a class from the TThread class (going back to the Delphi 7 good old days), but there is a far easier way to code it, by using the TTask.Run method associated with an anonymous method:

12345
TTask.Run(
procedure
begin
ReadBatchFiles;
end);

As a result, the ReadBatchFiles will be run in a separate thread, releasing the main thread for the user to keep working on the application. We could say that the method will be executed as a “background task”. At the end of the process, you can even use a TNotificationCenter component to notify the user that the process is finished.

Alternatively, you can use TThread.CreateAnonymousThread, which will perform the same action:

12345
TThread.CreateAnonymousThread(
procedure
begin
ReadBatchFiles;
end);

It’s important to state that there are more classes and functions to work with parallel programming, such as the TParallel.For, which provides a way to “split” a For loop in threads, and IFuture, which allows us to run a function in parallel and get a result to use in a later moment.

If you are interested in this topic, we recommend you explore the System.Threading unit. And if you need Delphi software modernization, contact us.

Subscribe to our newsletter and get amazing content right in your inbox.

This field is required
This field is required Invalid email address

Thank you for subscribing!
See you soon... in your inbox!

confirm your subscription, make sure to check your promotions/spam folder

Tags

Subscribe to our newsletter and get amazing content right in your inbox.

You can unsubscribe from the newsletter at any time

This field is required
This field is required Invalid email address

You're almost there...

A confirmation was sent to your email

confirm your subscription, make sure to check
your promotions/spam folder