Difference between revisions of "Asynchronous Calls"

From Lazarus wiki
Jump to navigationJump to search
(added thread safe GUI update example)
Line 3: Line 3:
 
== Problem statement ==
 
== Problem statement ==
  
When handling some event, you need to do something, but you can't do it right away. Example: you need to free an object, but it is or will be referenced somewhere in the parent (or its parent etc.) later on.
+
When handling some event, you need to do something, but you can't do it right away. For example, you need to free an object, but it is or will be referenced somewhere in the parent (or its parent etc.) later on. Or you need to update GUI elements accessed by several threads in a thread safe manner.
  
 
== Solution ==
 
== Solution ==
Line 17: Line 17:
 
Note that this is a more generic version of [[doc:lcl/forms/tapplication.releasecomponent.html |ReleaseComponent]], and ReleaseComponent calls this method.
 
Note that this is a more generic version of [[doc:lcl/forms/tapplication.releasecomponent.html |ReleaseComponent]], and ReleaseComponent calls this method.
  
== Example ==
+
== Examples ==
 +
 
 +
=== Simple data passed to async function ===
  
 
The following program shows the use of [[doc:lcl/forms/tapplication.queueasynccall.html |QueueAsyncCall]]. If you press on the CallButton, 'Click 1', 'Click 2' and 'Async 1' is added to the LogListBox. Note that Async call is only executed after the CallButtonClick event has finished.
 
The following program shows the use of [[doc:lcl/forms/tapplication.queueasynccall.html |QueueAsyncCall]]. If you press on the CallButton, 'Click 1', 'Click 2' and 'Async 1' is added to the LogListBox. Note that Async call is only executed after the CallButtonClick event has finished.
Line 70: Line 72:
  
 
end.</syntaxhighlight>
 
end.</syntaxhighlight>
 +
 +
=== Record passed to async function ===
 +
 +
Here is some code extracted from MultiLog's MemoChannel unit used for thread safe logging messages to Memo control. All writings to Memo are serialized. There are no locks and no conflicts, even with hundreds of threads. Of course, record can hold much more then a simple string.
 +
 +
<syntaxhighlight>
 +
...
 +
type
 +
  TLogMsgData = record
 +
    Text: string;
 +
  end;
 +
  PLogMsgData = ^TLogMsgData;
 +
...
 +
procedure TMemoChannel.Write(const AMsg: string);
 +
var
 +
  LogMsgToSend: PLogMsgData;
 +
begin
 +
  New(LogMsgToSend);
 +
  LogMsgToSend^.Text:= AMsg;
 +
  Application.QueueAsyncCall(@WriteAsyncQueue, PtrInt(LogMsgToSend)); // put log msg into queue that will be processed from the main thread after all other messages
 +
end;
 +
...
 +
procedure TMemoChannel.WriteAsyncQueue(Data: PtrInt);
 +
var // called from main thread after all other messages have been processed to allow thread safe TMemo access
 +
  ReceivedLogMsg: TLogMsgData;
 +
begin
 +
  ReceivedLogMsg := PLogMsgData(Data)^;
 +
  try
 +
    if (FMemo <> nil) and (not Application.Terminated) then
 +
    begin
 +
      ...
 +
      FMemo.Append(ReceivedLogMsg.Text) // <<< fully thread safe
 +
    end;
 +
  finally
 +
    Dispose(PLogMsgData(Data));
 +
  end;
 +
end;
 +
</syntaxhighlight>
  
 
==See also==
 
==See also==

Revision as of 09:26, 19 December 2018

English (en) français (fr) 日本語 (ja) русский (ru)

Problem statement

When handling some event, you need to do something, but you can't do it right away. For example, you need to free an object, but it is or will be referenced somewhere in the parent (or its parent etc.) later on. Or you need to update GUI elements accessed by several threads in a thread safe manner.

Solution

Call Application.QueueAsyncCall:

TDataEvent = procedure (Data: PtrInt) of object;

procedure QueueAsyncCall(AMethod: TDataEvent; Data: PtrInt);

This will "queue" the given method with the given parameter for execution in the main event loop, when all other events have been processed. In the example above, the reference to the object you wanted to free has gone, since the then-parent has finished execution, and the object you wanted to free can be freed safely.

Note that this is a more generic version of ReleaseComponent, and ReleaseComponent calls this method.

Examples

Simple data passed to async function

The following program shows the use of QueueAsyncCall. If you press on the CallButton, 'Click 1', 'Click 2' and 'Async 1' is added to the LogListBox. Note that Async call is only executed after the CallButtonClick event has finished.

unit TestQueueAsyncCall;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, Buttons,
  StdCtrls;

type

  { TQueueAsyncCallForm }

  TQueueAsyncCallForm = class(TForm)
    CallButton: TButton;
    LogListBox: TListBox;
    procedure CallButtonClick(Sender: TObject);
  private
    { private declarations }
    FCounter: PtrInt;
    procedure Async(Data: PtrInt);
  public
    { public declarations }
  end; 

var
  QueueAsyncCallForm: TQueueAsyncCallForm;

implementation

{$R *.lfm}

{ TQueueAsyncCallForm }

procedure TQueueAsyncCallForm.CallButtonClick(Sender: TObject);
begin
  LogListBox.Items.Add('Click 1');
  FCounter := FCounter+1;
  Application.QueueAsyncCall(@Async,FCounter);
  LogListBox.Items.Add('Click 2');
end;

procedure TQueueAsyncCallForm.Async(Data: PtrInt);
begin
   LogListBox.Items.Add('Async '+ IntToStr(Data));
end;

end.

Record passed to async function

Here is some code extracted from MultiLog's MemoChannel unit used for thread safe logging messages to Memo control. All writings to Memo are serialized. There are no locks and no conflicts, even with hundreds of threads. Of course, record can hold much more then a simple string.

...
type
  TLogMsgData = record
    Text: string;
  end;
  PLogMsgData = ^TLogMsgData;
...
procedure TMemoChannel.Write(const AMsg: string);
var
  LogMsgToSend: PLogMsgData;
begin
  New(LogMsgToSend);
  LogMsgToSend^.Text:= AMsg;
  Application.QueueAsyncCall(@WriteAsyncQueue, PtrInt(LogMsgToSend)); // put log msg into queue that will be processed from the main thread after all other messages
end;
...
procedure TMemoChannel.WriteAsyncQueue(Data: PtrInt);
var // called from main thread after all other messages have been processed to allow thread safe TMemo access
  ReceivedLogMsg: TLogMsgData;
begin
  ReceivedLogMsg := PLogMsgData(Data)^;
  try
    if (FMemo <> nil) and (not Application.Terminated) then
    begin
      ...
      FMemo.Append(ReceivedLogMsg.Text) // <<< fully thread safe
    end;
  finally
    Dispose(PLogMsgData(Data));
  end;
end;

See also