Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Expert Delphi

You're reading from   Expert Delphi Robust and fast cross-platform application development

Arrow left icon
Product type Paperback
Published in Feb 2024
Publisher Packt
ISBN-13 9781805121107
Length 424 pages
Edition 2nd Edition
Languages
Arrow right icon
Authors (2):
Arrow left icon
Marco Cantù Marco Cantù
Author Profile Icon Marco Cantù
Marco Cantù
Paweł Głowacki Paweł Głowacki
Author Profile Icon Paweł Głowacki
Paweł Głowacki
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Part 1: Building Blocks FREE CHAPTER
2. Chapter 1: Fasten Your Seat Belts 3. Chapter 2: Mind Your Language 4. Chapter 3: Packing Up Your Toolbox 5. Chapter 4: Using the Parallel Programming Library 6. Part 2: Going Mobile
7. Chapter 5: Playing with FireMonkey 8. Chapter 6: FireMonkey in 3D 9. Chapter 7: Building User Interfaces with Style 10. Chapter 8: Working with Mobile Operating Systems 11. Chapter 9: Desktop Apps and Mobile Bridges 12. Part 3: From Data to Services
13. Chapter 10: Embedding Databases 14. Chapter 11: Integrating with Web Services 15. Chapter 12: Building Mobile Backends 16. Chapter 13: Easy REST API Publishing with RAD Server 17. Chapter 14: App Deployment 18. Chapter 15: The Road Ahead 19. Index
20. Other Books You May Enjoy

Exploring the Parallel Programming Library

On top of this core foundation, Delphi offers the more abstract Parallel Programming Library (PPL). This library allows you to express your application logic in a way that is less tied to the actual hardware and the capabilities of the CPU the program is running into. To clarify, the core element of the PPL is the TTask class; this is conceptually similar to a thread but abstracted from the hardware. For example, if you create 20 threads, you are making requests to the operating system and the CPU. If you create 20 tasks, the PPL will create some threads in a pool, depending on the CPU multicore capabilities, and assign tasks to those threads, without overloading the CPU. Also, reusing threads saves time, as thread creation is a heavy operation at the operating system level.

The very first thing to do in order to use the PPL is to add the System.Threading unit to the uses clause of your program. As mentioned, on behalf of the app, the library maintains a self-tuning pool of threads that are used to execute tasks.

In the following sections, we’ll look at the high-level features that PPL offers to Delphi developers.

Parallel loops

The easiest concept to grasp in the PPL is the parallel for loop. This is useful when calculations for different values of a control variable are independent and it is not important in which order they are executed. A good example of this use case comes from ray tracing algorithms used in computer graphics. To generate an image, we need to calculate the color of each pixel that makes up the resulting image. This is done by calculating the path of each ray of light in space. Calculating the color of a given pixel is completely independent of other pixels and can be performed simultaneously to generate the resulting bitmap faster.

Instead of implementing a ray tracer here, let’s create a truly simple demo that will allow us to observe how much faster a parallel loop would execute compared to a traditional loop. Instead of calculating pixel colors in each iteration of the loop, we will just call the Sleep procedure, which will make the current thread sleep for a given number of milliseconds and then it will continue to run.

Let’s create a complete example using a parallel loop from scratch. First, create a new multi-device project in Delphi. Next, drop two buttons on the form and write the following code; in the caption of each button, this will display how much time it took to execute the loop. To calculate the elapsed time, we are using a TStopWatch record type from the System.Diagnostics unit. This is the code used to perform a slow operation:

function TForm1.DoTimeConsumingOperation(Length: integer): Double;
begin
  var Tot := 1.0;
  for var I := 1 to 10_000 * Length do
     // some random slow math operation
     Tot := Log2(Tot) + Sqrt(I);
  Result := Tot;
end;

Now, let’s write a regular for loop using this slow operation and check how much time it takes:

procedure TForm1.btnForLoopRegularClick(Sender: TObject);
var
  SW: TStopwatch;
begin
  SW := TStopwatch.StartNew;
  for var I := 0 to 99 do
    DoTimeConsumingOperation(10);
  SW.Stop;
  (Sender as TButton).Text :=
    SW.ElapsedMilliseconds.ToString + 'ms';
end;

By comparison, this is the same code using a parallel for loop:

procedure TForm1.btnForLoopParallelClick(Sender: TObject);
var
  SW: TStopwatch;
  I: Integer;
begin
  SW := TStopwatch.StartNew;
  TParallel.For(0, 99, procedure(I: integer)
  begin
    DoTimeConsumingOperation(10);
  end);
  SW.Stop;
  (Sender as TButton).Text :=
    SW.ElapsedMilliseconds.ToString + 'ms';
end;

The TParallel.For class method we are using in the preceding code has many overloaded versions. In this example, it takes the starting index, ending index, and an anonymous procedure that takes an integer parameter for the index.

On my Windows PC, for a 64-bit application, the regular for loop takes about half a second (425 milliseconds, on average) while the parallel version takes only 35 milliseconds, about one-tenth of the time. While the absolute speed depends on your CPU power, the difference between the two timings depends on the number of cores available to your CPU.

By comparison on my Android phone, the same app compiled for 64-bit takes 2,814 milliseconds for the regular loop and only 670 milliseconds for the parallel version. Your time will vary depending on the device, but the difference between the two versions should remain significant (unless you run on virtualized hardware with only a few available cores).

Using tasks

One of the key use cases for multithreading code is to keep the user interface responsive while performing some long-running operation, such as downloading data from the internet or performing calculations. The main thread of the application (the one where the user interface runs) should not be busy with those time-consuming operations. They should be executed in the background thread to keep the user interface responsive.

Now, you can add two more buttons to the form and enter the following code in their OnClick events:

procedure TForm1.btnNonResponsiveClick(Sender: TObject);
begin
  DoTimeConsumingOperation(3_000);
  (Sender as TButton).Text := 'Done';
end;
procedure TForm1.btnResponsive1Click(Sender: TObject);
var
  ATask: ITask;
begin
  ATask := TTask.Create(procedure
  begin
    DoTimeConsumingOperation(3_000);
  end);
  ATask.Start;
  (Sender as TButton).Text := 'Done';
end;

When you click the first button, the form freezes for a few seconds. When you click on the second button, the form stays responsive because the time-consuming operation is executed in a different thread and the main app thread can process user events normally. In fact, the Done message is displayed almost immediately because the main thread continues to execute just after it starts the task. If you would like to display the Done text on the button after the task is complete, you would need to do it from inside the background thread. You can only manipulate the user interface from the main app thread, which is why the call to change the button’s Text property needs to be synchronized:

procedure TForm1.btnResponsive2Click(Sender: TObject);
var
  ATask: ITask;
begin
  ATask := TTask.Create(procedure
  begin
    DoTimeConsumingOperation(3000);
    TThread.Synchronize(nil,
      procedure
      begin
       (Sender as TButton).Text := 'Done';
      end);
  end);
  ATask.Start;
end;

Now, the label doesn’t change immediately, but after a few seconds, when the operation completes. A task is a bit like an anonymous procedure. You can declare it, but it only starts to execute when it is told to. You need to call the Start method, and only then will the task start.

The beauty of futures

The PPL also provides the notion of a “future.” This is a specialization of a task generally tied to calculations or mathematical operations. A future is a task that returns a value. In the case of a task, you call its Start method to execute. In the case of a future, you can just declare it and assign it to a variable. The PPL will execute the code when it has a thread available. When you try to retrieve the value of this variable, if the future has already been executed, the value is returned; if the future is still pending, it is executed as soon as possible and the program will wait for its completion. In any case, when you access the value of the future, you’ll read the value.

There are some interesting use cases when futures are used. For example, you may want to calculate something based on values of two or more parameters that need to be calculated first. Instead of calculating these parameters sequentially, we could perform these calculations in parallel.

For this demo, let’s reuse the calculations done earlier and read the resulting value. Let’s assume we have to calculate the value for three different parameters and add them at the end.

Let’s first have a look at how we could implement this functionality without futures. Here is an example using standard code (I’ve omitted the lines for timing and for displaying results):

var
  First, Second, Third: Double;
begin
  First := DoTimeConsumingOperation(200);
  Second := DoTimeConsumingOperation(300);
  Third := DoTimeConsumingOperation(400);
  var GrandTotal := First + Second + Third;

GrandTotal has to be computed by performing all the previous calculations first; on my PC, this code takes about 389 milliseconds.

You can implement the same functionality using futures. In this case, the code will look like this:

var
  First, Second, Third: IFuture<Double>;
begin
  First := TTask.Future<Double>(function: Double
  begin
    Result := DoTimeConsumingOperation(200);
  end);
  Second := TTask.Future<Double>(function: Double
  begin
    Result := DoTimeConsumingOperation(300);
  end);
  Third := TTask.Future<Double>(function: Double
  begin
    Result := DoTimeConsumingOperation(400);
  end);
  var GrantTotal: Double :=
    First.Value + Second.Value + Third.Value;

This code wraps each of the three operations in a future by providing the corresponding code in an anonymous method. At the end, you need to read the result of that calculation using the Value property of each IFuture<Double> variable.

Executing this code on the same PC requires about 176 milliseconds. This is about half and it’s possibly the time it takes for the slowest of the three calculations: the one with 400 as a parameter.

So, what’s the difference between a task and a future? A task is an operation or a procedure that you want to be executed in a thread. A future is a calculation or a function that you want to be executed in a thread.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime