Sending messages from a thread

The second part of the solution deals with sending messages back to the parent. As I've mentioned before, you already have several possibilities to do the same with built-in Delphi functions. They all work well in combination with the previously described method for sending messages to a thread. This second part is therefore purely optional, but I believe it is a good solution, because it centralizes the message-handling for each thread.

Again, the code starts by creating a TThreadedQueue<TValue> to store sent values. We cannot use an event to signal new messages, though. Delphi forms interact with the operating system and with the user by constantly processing messages, and we cannot block this message processing by waiting on an event, or the whole user interface will freeze. Instead of that, TCommThread uses Queue to call a method of your own choosing to process messages. You pass this method to the constructor as a parameter.

The following code fragment shows relevant types and fields, together with (now complete) constructor. Besides the user-provided MessageReceiver method that will process messages, you can also set the queue size:

public type
TMessageReceiver = TProc<TValue>;
strict private
FQueueToMain : TThreadedQueue<TValue>;
FMessageReceiver: TMessageReceiver;

constructor TCommThread.Create(MessageReceiver: TMessageReceiver;
CreateSuspended: boolean = False;
ToThreadQueueSize: integer = 1024; ToMainQueueSize: integer = 1024);
begin
inherited Create(CreateSuspended);
FQueueToThread := TThreadedQueue<TValue>.Create(ToThreadQueueSize, 0, 0);
Event := TEvent.Create;
FMessageReceiver := MessageReceiver;
if assigned(MessageReceiver) then
FQueueToMain := TThreadedQueue<TValue>.Create(ToMainQueueSize, 0, 0);
end;

The message-receiver parameter is optional. If you set it to nil, the channel for sending messages back to the owner won't be created, and this part of the functionality will be disabled.

The code also overrides the standard TThread constructor to create a  TCommThread with only one communication channel—the one towards the thread:

constructor TCommThread.Create(CreateSuspended: Boolean);
begin
Create(nil, CreateSuspended);
end;

The code can use the protected method, SendToMain, to send messages back to the owner. It checks whether the necessary queue was created, posts a message to the queue, and, if that was successful, queues a PushMessagesToReceiver method. If the queue is full, this method returns False:

function TCommThread.SendToMain(const value: TValue): boolean;
begin
if not assigned(FQueueToMain) then
raise Exception.Create('MessageReceiver method was not set in constructor!');
Result := (FQueueToMain.PushItem(value) = wrSignaled);
if Result then
TThread.Queue(nil, PushMessagesToReceiver);
end;

The PushMessagesToReceiver method is implemented in the TCommThread object. It reads messages from the queue, and, for each message, calls the user-provided FMessageReceiver method:

procedure TCommThread.PushMessagesToReceiver;
var
value: TValue;
begin
// This method executes from the main thread!
while FQueueToMain.PopItem(value) = wrSignaled do
FMessageReceiver(value);
end;

The value of this approach is twofold. Firstly, all message processing for a thread is concentrated in one method. Secondly, you can pass different types of data through this channel—from basic types to objects, interfaces, and even records.

A word of caution is necessary. Never destroy the thread object from inside the message receiver. Instead, use the ForceQueue method to queue a code to destroy the thread object. That will cause that code to execute after the message receiver does its job.

In the ThreadCommMain unit, all this comes together. The Start thread button creates a new thread and immediately sends a number 42 to it. It only sets the message receiver method (ProcessThreadMessages) and leaves other parameters at default:

procedure TfrmThreadComm.btnStartClick(Sender: TObject);
begin
FThread := TMyThread.Create(ProcessThreadMessages);
FThread.SendToThread(42);
end;

The Change button sends the current value of a spin edit to the thread via the communication mechanism:

procedure TfrmThreadComm.btnChangePingClick(Sender: TObject);
begin
FThread.SendToThread(inpPing.Value);
end;

The Stop thread button just destroys the thread object:

procedure TfrmThreadComm.btnStopClick(Sender: TObject);
begin
FreeAndNil(FThread);
end;

The TMyThread object overrides the Execute method. We've seen a part of that method before. The full implementation is shown here.

The code immediately sends a message of a TPingMsg type back to the owner. This message carries the current value of the local variable, pingMsg, and the current thread ID. The message is sent over the built-in communication mechanism.

Next, the code starts the while loop, which I have already described. The only new part here appears under the // workload comment, when the code again sends the current pingMsg and thread ID to the owner:

type
TPingMsg = TPair<integer,cardinal>;

procedure TMyThread.Execute;
var
pingMsg: integer;
value: TValue;
begin
pingMsg := 0;
if not SendToMain(TValue.From<TPingMsg>(
TPingMsg.Create(pingMsg, ThreadID)))
then
raise Exception.Create('Queue full!');
while Event.WaitFor(5000) in [wrSignaled, wrTimeout] do
begin
Event.ResetEvent;
// message processing
while (not Terminated) and GetMessage(value) do
pingMsg := value.AsInteger;
// termination
if Terminated then
break;
// workload
if not SendToMain(TValue.From<TPingMsg>(
TPingMsg.Create(pingMsg, ThreadID)))
then
raise Exception.Create('Queue full!');
end;
end;

The behavior of this method can be described in a few words. It sends pingMsg back to the owner every five seconds. When it receives a message, it assumes that it contains an integer and stores that integer in the pingMsg variable. The code terminates when the Terminated flag is set.

The only part I haven't shown yet is the ProcessThreadMessages method, which simply shows the data stored inside the TPingMsg:

procedure TfrmThreadComm.ProcessThreadMessages(value: TValue);
var
pingMsg: TPingMsg;
begin
pingMsg := value.AsType<TPingMsg>;
ListBox1.Items.Add(Format('%s: %d from thread %d',
[FormatDateTime('hh:nn:ss.zzz', Now), pingMsg.Key, pingMsg.Value]));
end;

And there you have it—a thread with two communication channels!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.144.48