In this article by Daniele Teti author of the book Delphi Cookbook - Second Edition we will study about Multithreading. Multithreading can be your biggest problem if you cannot handle it with care. One of the fathers of the Delphi compiler used to say:
"New programmers are drawn to multithreading like moths to flame, with similar results."
– Danny Thorpe
(For more resources related to this topic, see here.)
In this chapter, we will discuss some of the main techniques to handle single or multiple background threads. We'll talk about shared resource synchronization and thread-safe queues and events. The last three recipes will talk about the Parallel Programming Library introduced in Delphi XE7, and I hope that you will love it as much as I love it. Multithreaded programming is a huge topic. So, after reading this chapter, although you will not become a master of it, you will surely be able to approach the concept of multithreaded programming with confidence and will have the basics to jump on to more specific stuff when (and if) you require them.
Using a background thread and working with its private data is not difficult, but safely bringing information retrieved or elaborated by the thread back to the main thread to show them to the user (as you know, only the main thread can handle the GUI in VCL as well as in FireMonkey) can be a daunting task. An even more complex task would be establishing a generic communication between two or more background threads. In this recipe, you'll see how a background thread can talk to the main thread in a safe manner using the TThreadedQueue<T> class. The same concepts are valid for a communication between two or more background threads.
Let's talk about a scenario. You have to show data generated from some sort of device or subsystem, let's say a serial, a USB device, a query polling on the database data, or a TCP socket. You cannot simply wait for data using TTimer because this would freeze your GUI during the wait, and the wait can be long. You have tried it, but your interface became sluggish… you need another solution!
In the Delphi RTL, there is a very useful class called TThreadedQueue<T> that is, as the name suggests, a particular parametric queue (a FIFO data structure) that can be safely used from different threads. How to use it? In the programming field, there is mostly no single solution valid for all situations, but the following one is very popular. Feel free to change your approach if necessary. However, this is the approach used in the recipe code:
Open the recipe project called ThreadingQueueSample.dproj. This project contains the main form with all the GUI-related code and another unit with the thread code.
The FormCreate event creates the shared queue with the following parameters that will influence the behavior of the queue:
The button labeled Start Thread creates a TReaderThread instance passing the already created queue to its constructor (this is a particular type of dependency injection called constructor injection).
The thread declaration is really simple and is as follows:
type
TReaderThread = class(TThread)
private
FQueue: TThreadedQueue<Byte>;
protected
procedure Execute; override;
public
constructor Create(AQueue: TThreadedQueue<Byte>);
end;
While the Execute method simply appends randomly generated data to the queue, note that the Terminated property must be checked often so the application can terminate the thread and wait a reasonable time for its actual termination. In the following example, if the queue is not empty, check the termination at least every 700 msec ca:
procedure TReaderThread.Execute;
begin
while not Terminated do
begin
TThread.Sleep(200 + Trunc(Random(500)));
// e.g. reading from an actual device
FQueue.PushItem(Random(256));
end;
end;
So far, you've filled the queue. Now, you have to read from the queue and do something useful with the read data. This is the job of a timer. The following is the code of the timer event on the main form:
procedure TMainForm.Timer1Timer(Sender: TObject);
var
Value: Byte;
begin
while FQueue.PopItem(Value) = TWaitResult.wrSignaled do
begin
ListBox1.Items.Add(Format('[%3.3d]', [Value]));
end;
ListBox1.ItemIndex := ListBox1.Count - 1;
end;
That's it! Run the application and see how we are reading the data coming from the threads and showing the main form. The following is a screenshot:
The main form showing data generated by the background thread
The TThreadedQueue<T> is very powerful and can be used to communicate between two or more background threads in a consumer/producer schema as well. You can use multiple producers, multiple consumers, or both. The following screenshot shows a popular schema used when the speed at which the data generated is faster than the speed at which the same data is handled. In this case, usually you can gain speed on the processing side using multiple consumers.
Single producer, multiple consumers
In this article we had a look at how to talk to the main thread using a thread-safe queue.