1

In using threading/PPL to enhance performance of an enterprise FMX app, I experienced some issues but could not find explanations for. Here is a simplified program to show what the issues are:

Form content

object Form1: TForm1
  FormFactor.Devices = [Desktop]
  object Label1: TLabel
    Text = 'Time'
  end
  object Label2: TLabel
    Text = 'Timer state'
  end
  object Button1: TButton
    Text = 'Start (Normal)'
    OnClick = Button1Click
  end
  object Button2: TButton
    Text = 'Start (Task)'
    OnClick = Button2Click
  end
  object Button3: TButton
    Text = 'Start (AnonymThread)'
    OnClick = Button3Click
  end
  object Timer: TTimer
    Enabled = False
    OnTimer = TimerTimer
  end
end

Form code

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
  FMX.Controls.Presentation;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Timer: TTimer;
    procedure TimerTimer(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses System.Threading;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Timer.Enabled:= False;
  Label1.Text:= 'Time (Normal)';

  Timer.Enabled:= True;
  Label2.Text:= 'Timer ON (Normal)';

  // Result: All above and OnTimer OK

end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Timer.Enabled:= False;
  Label1.Text:= 'Time (Task)';

  TTask.Run(
    procedure
    begin
      Timer.Enabled:= True;
      Label2.Text:= 'Timer ON (Task)';
    end);

  // Result: All above OK but no OnTimer

end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  Timer.Enabled:= False;
  Label1.Text:= 'Time (AnonymThread)';

  TThread.CreateAnonymousThread(
    procedure
    begin
      Timer.Enabled:= True;
      Label2.Text:= 'Timer ON (AnonymThread)';
    end).Start;

  // Result: ThreadProc may/may not fully run (But Timer.Enabled:= True seems always executed) and no OnTimer

end;

procedure TForm1.TimerTimer(Sender: TObject);
begin
  Label1.Text:= DateTimeToStr(Now);
end;

end.

Note: In each button's OnClick I have written the consequent result.

Could any fellow Delphi programmer help me to understand why and, possibly, what to do (Using Threading/PPL, of course)? Thank you in advance.

user3707922
  • 11
  • 1
  • 1

1 Answers1

2

One of the basic rules of multi-threading is that you mustn't interact with the GUI from a non-GUI thread.

This includes timers you drop on a form. For example, on Microsoft Windows, enabling a timer will call SetTimer. In FMX, this timer is set up with a callback procedure. Thus, the following part of the documentation is relevant:

When you specify a TimerProc callback function, the default window procedure calls the callback function when it processes WM_TIMER. Therefore, you need to dispatch messages in the calling thread, even when you use TimerProc instead of processing WM_TIMER.

Your OnTimer event isn't fired because your new thread isn't dispatching messages. You need to redesign your system. In particular, don't touch a GUI-based timer from a non-GUI thread. And don't touch any control like a TLabel.

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • A `TTimer` wouldn't work across thread boundaries anyway, at least in VCL (not sure how FMX implements its timers). VCL's `TTimer` constructor creates a hidden `HWND`, and an `HWND` can receive and dispatch messages only in the thread that creates it. So even if another thread called `SetTimer()` for that `HWND`, a message loop in that thread would never see the `WM_TIMER` messages. – Remy Lebeau Feb 06 '21 at 16:34
  • @RemyLebeau: Yes, the VCL and Win32-FMX `TTimer` implementations are different. While VCL supplies a window handle to `SetTimer`, FMX supplies a callback function. Haven't studied the FMX variant much, though. (Actually, I had never studied it at all before I saw this Q. I try to stay as far away from FMX as possible.) – Andreas Rejbrand Feb 06 '21 at 16:42
  • "*I try to stay as far away from FMX as possible*" - as do I – Remy Lebeau Feb 06 '21 at 17:30
  • apparently once upon a time I did know how FMX'S TTimer works (at least on Android): https://stackoverflow.com/a/45949720/65863 – Remy Lebeau Feb 06 '21 at 17:33
  • Thank you all very much. I do know that I should not touch UI controls from a thread but use a TLabel to do quick-and-dirty checks for my simple program only. As my Timer is constructed in the program's main thread, I just thought setting its Enabled property will allow it to work in the main loop as normal :) Anyway, I will try to re-design my app then. – user3707922 Feb 06 '21 at 21:04
  • @user3707922: Unfortunately not. But you can of course post or send a message from your new thread to the GUI thread and let the receiver start the timer. – Andreas Rejbrand Feb 06 '21 at 21:08
  • 1
    Yes, that would work. By the way, for the sake of investigation, I found that putting "Timer.Enabled:= True" in "TThread.Synchronize(TThread.Current, ...)" or "TThread.Queue(TThread.Current, ...)" makes all work OK too, although it feels funny somehow :) Thanks! – user3707922 Feb 06 '21 at 22:28
  • @AndreasRejbrand You should include the information from the last comment of the QP to your answer. – Delphi Coder Feb 07 '21 at 08:25
  • Don't touch UI from a thread, means Don't touch UI from a thread, EVER. It is not meant to be used for debugging purposes, because it can change behavior of the code you are observing. Use OutputDebugString instead. – Dalija Prasnikar Feb 07 '21 at 12:18
  • Everyday a new thing to two to learn :) Thanks. – user3707922 Feb 07 '21 at 17:22