Windows messages

 The first method works by sending Windows messages to the main thread. As the name suggests, it is limited to the Windows operating system. I'll show two ways to achieve the same result. The first works only in VCL applications, while the second also works with FireMonkey.

Before the tasks are started, the code initializes the result FValue to zero so that partial results can be aggregated correctly. It also sets the variable FNumDone to zero. This variable counts the tasks that have completed their work:

procedure TfrmIncDecComm.btnMessageClick(Sender: TObject);
begin
FValue := 0;
FNumDone := 0;
RunInParallel(IncMessage, DecMessage);
end;

Both tasks are implemented in the same way, so I'll only show one method here. IncMessage increments the starting value CNumRepeat times. At the end, it posts a MSG_TASK_DONE message back to the main form and passes the resulting value as a parameter. DecMessage looks the same except that the value is decremented inside the for loop:

procedure TfrmIncDecComm.IncMessage(startValue: integer);
var
i: integer;
value: integer;
begin
value := startValue;
for i := 1 to CNumRepeat do
value := value + 1;
PostMessage(Handle, MSG_TASK_DONE, value, 0);
end;
Message parameters wParam and lParam can be used to pass data alongside the message. They have the same size as a pointer, which allows us to use an object or an interface as data storage. I'll show an example of such code near the end of this section.

To process this message in the main form, we have to declare the MSG_TASK_DONE variable (WM_USER indicates the start of the message range reserved for custom use) and add a message-handling method MsgTaskDone to the form:

const
MSG_TASK_DONE = WM_USER;

procedure MsgTaskDone(var msg: TMessage); message MSG_TASK_DONE;

This method is called every time the main form receives the message, MSG_TASK_DONE. Inside, we firstly increment the FValue value by the partial result, passed in the msg.WParam field (aggregation step). After that, we increment the FNumDone counter. We know that we will receive exactly two messages, so we can call the cleanup method, Done, when FNumDone is equal to 2:

procedure TfrmIncDecComm.MsgTaskDone(var msg: TMessage);
begin
Inc(FValue, msg.WParam);
Inc(FNumDone);
if FNumDone = 2 then
Done('Windows message');
end;

In the Done method, we only have to clean the worker task interfaces:

procedure TfrmIncDecComm.Done(const name: string);
begin
FTasks[0] := nil;
FTasks[1] := nil;
end;

This seems like a lot of work, but it is worth it. I'll show the actual results after I finish discussing all four methods, but I can already tell you that this approach is finally faster than the single-threaded code.

This method of declaring a message handler is specific to VCL. On FireMonkey, you have to invest a bit more work and create a hidden window which will receive and process messages.

When you click on a Message + AllocateHwnd button, the following code executes. It initializes FValue and FNumDone, as in the previous example, and then creates a hidden window by calling AllocateHwnd:

procedure TfrmIncDecComm.btnAllocateHwndClick(Sender: TObject);
begin
FValue := 0;
FNumDone := 0;
FMsgWnd := AllocateHwnd(MsgWndProc);
Assert(FMsgWnd <> 0);
RunInParallel(IncMsgHwnd, DecMsgHwnd);
end;

The task method, IncMsgHwnd, looks the same as IncMethod, except the last line which must now post the message to the handle FMsgWnd:

PostMessage(FMsgWnd, MSG_TASK_DONE, value, 0);

The MsgWndProc method processes all messages sent to this hidden window. We are only interested in MSG_TASK_DONE. Any other messages are passed to the Windows function, DefWindowProc. The code that handles MSG_TASK_DONE is identical to code in the MsgTaskDone method:

procedure TfrmIncDecComm.MsgWndProc(var msg: TMessage);
begin
if Msg.Msg = MSG_TASK_DONE then
begin
Inc(FValue, msg.WParam);
Inc(FNumDone);
if FNumDone = 2 then
Done('Windows message');
end
else
DefWindowProc(FMsgWnd, msg.Msg, msg.wParam, msg.lParam);
end;

To clean up this hidden window, we have to call the DeallocateHwnd method:

if FMsgWnd <> 0 then
begin
DeallocateHwnd(FMsgWnd);
FMsgWnd := 0;
end;

I promised before to show how you can send an object or an interface as message parameters, so here's a short example. To send an object, we simply have to cast it to the appropriate type. In Delphi 10.2 Tokyo that is NativeUInt for wParam and NativeInt for lParam.

If we did the same with an interface, it would be destroyed on exit from btnObjIntClick. To prevent that, we have to increase its reference count by calling _AddRef:

procedure TfrmIncDecComm.btnObjIntClick(Sender: TObject);
var
tobj: TObject;
iint: IInterface;
begin
tobj := TObject.Create;
iint := TInterfacedObject.Create;
iint._AddRef;
PostMessage(Handle, MSG_OBJ_INT, NativeUInt(tobj), NativeInt(iint));
end;

On the receiving side, we can simply cast the object back to the appropriate object type (TObject in this example). We also have to remember to destroy it when we're done with it.

An interface can also be cast to the appropriate interface type (IInterface in this example) but then we also have to remove the reference added in btnObjIntClick by calling _Release. We can then use this interface as we need, and it will be destroyed once it is no longer in use and its reference count drops to zero. The following code demonstrates this:

procedure TfrmIncDecComm.MsgObjInt(var msg: TMessage);
var
tobj: TObject;
iint: IInterface;
begin
tobj := TObject(msg.WParam);
tobj.Free;
iint := IInterface(msg.LParam);
iint._Release;
end;
..................Content has been hidden....................

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