Can I safely enable TTimer from a non-UI thread?

Viewed 277

I'm using Delphi XE7 on Windows 10.

I have been using the following code for a long time, and just read the documentation on SetTimer(). To state it simply, I am setting timers from non-UI threads, but Microsoft's documentation says they should only be set on the UI thread. Extensive tests show my code works fine, but I can't trust my system to behave the same as other systems, or the Microsoft documentation to be 100% accurate. Can anyone verify whether this code is OK or not OK?

The Delphi code will not deadlock, it pretty much just calls SetTimer() (I am aware there is a race condition setting TTimer.FEnabled).

The MSDN documentation says:

hWnd

Type: HWND

A handle to the window to be associated with the timer. This window must be owned by the calling thread.

What I'm trying to accomplish is worker threads doing stuff, and when appropriate, they notify the main thread that elements of the UI must be updated, and the main thread updates the UI. I know how to use TThread.Synchronize(), but deadlocks can happen in certain cases. I can use PostMessage() from my worker threads and handle the message in the UI thread.

Is there any other way in Delphi to notify and update the UI thread?

unit FormTestSync;

interface
uses SysUtils, Classes, Forms, StdCtrls, ExtCtrls, Controls;

type
  TypeThreadTest = class(TThread)
    protected
      procedure Execute; override;
  end;

type
  TForm1 = class(TForm)
      timer_update: TTimer;
      Label1: TLabel;
      procedure timer_updateTimer(Sender: TObject);
      procedure FormCreate(Sender: TObject);
    private
      m_thread: TypeThreadTest;
      m_value: integer;

    private
      procedure Notify(value: integer);
    public
  end;

var
  Form1: TForm1;

implementation
{$R *.dfm}

procedure TypeThreadTest.Execute;
begin
  while (not terminated) do begin
    //do work...
    form1.Notify(random(MaxInt));
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  timer_update.enabled := false;
  timer_update.interval := 1;
  m_thread := TypeThreadTest.Create();
end;

procedure TForm1.Notify(value: integer);
begin
  //run on worker thread
  //Race conditions here, I left out the synchronization for simplicity
  m_value := value;
  timer_update.Enabled := true;
end;

procedure TForm1.timer_updateTimer(Sender: TObject);
begin
  timer_update.Enabled := false;
  label1.Caption := IntToStr(m_value);
end;

end.
2 Answers

The TTimer is being constructed in the main UI thread, when the TForm streams in its DFM resource. The TTimer's constructor creates an internal HWND for the timer to receive WM_TIMER messages with. That HWND is thus owned by the main UI thread.

TForm.Notify() is setting the timer's Enabled property to true, which will call SetTimer(). Notify() is being called in the context of the worker thread, not the main UI thread. This SHOULD NOT work, as stated in SetTimer()'s documentation. Only the main UI thread should be able to start the timer running, since the main UI thread owns the timer's HWND.

TTimer.UpdateTimer(), which is called internally by the setters of the timer's Enabled, Interval and OnTimer properties, will raise an EOutOfResources exception if SetTimer() fails. So, calling form1.Notify() in TypeThreadTest.Execute() SHOULD NOT work. The only way SetTimer() would not be called in that situation is if either:

  • Interval is 0
  • Enabled is false
  • OnTimer is unassigned

Otherwise, your worker thread SHOULD be crashing.

As you have noted, your worker thread can alternatively use TThread.Synchronize() (or TThread.Queue()), or PostMessage() (or SendMessage()), when it wants to notify the main UI thread to do something. These are viable and preferred solutions. Personally, I would opt for TThread.Queue(), eg:

unit FormTestSync;

interface

uses
  SysUtils, Classes, Forms, StdCtrls, ExtCtrls, Controls;

type
  TypeThreadTest = class(TThread)
  protected
    procedure Execute; override;
  end;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    m_thread: TypeThreadTest;
  private
    procedure Notify(value: integer);
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TypeThreadTest.Execute;
begin
  while not Terminated do begin
    //do work...
    Form1.Notify(random(MaxInt));
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  m_thread := TypeThreadTest.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  m_thread.Terminate;
  m_thread.WaitFor;
  m_thread.Free;
end;

procedure TForm1.Notify(value: integer);
begin
  //runs on worker thread
  TThread.Queue(nil,
    procedure
    begin
      //runs on main UI thread
      Label1.Caption := IntToStr(value);
    end
  );
end;

end.

If you want to use TTimer instead for this work, what you could do is simply enable the timer in the main UI thread and leave it enabled, and just synchronize access to the data that the timer accesses periodically. That would be perfectly safe, eg:

unit FormTestSync;

interface

uses
  SysUtils, Classes, Forms, StdCtrls, ExtCtrls, Controls, SyncObjs;

type
  TypeThreadTest = class(TThread)
  protected
    procedure Execute; override;
  end;

type
  TForm1 = class(TForm)
    timer_update: TTimer;
    Label1: TLabel;
    procedure timer_updateTimer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    m_thread: TypeThreadTest;
    m_value: integer;
    m_updated: boolean;
    m_lock: TCriticalSection;
  private
    procedure UpdateValue(value: integer);
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TypeThreadTest.Execute;
begin
  while not Terminated do begin
    //do work...
    Form1.UpdateValue(random(MaxInt));
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  m_lock := TCriticalSection.Create;
  timer_update.Interval := 100;
  timer_update.Enabled := true;
  m_thread := TypeThreadTest.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  m_thread.Terminate;
  m_thread.WaitFor;
  m_thread.Free;
  m_lock.Free;
end;

procedure TForm1.UpdateValue(value: integer);
begin
  //runs on worker thread
  m_lock.Enter;
  try
    m_value := value;
    m_updated := true;
  finally
    m_lock.Leave;
  end;
end;

procedure TForm1.timer_updateTimer(Sender: TObject);
begin
  //runs on main UI thread
  if m_updated then
  begin
    m_lock.Enter;
    try
      Label1.Caption := IntToStr(m_value);
      m_updated := false;
    finally
      m_lock.Leave;
    end;
  end;
end;

end.

UPDATE:

I did a quick test. When SetTimer() is called with a non-NULL HWND that is owned by another thread, sure enough on Windows XP, 7 and 10 (I did not test Vista or 8), SetTimer() succeeds, and the WM_TIMER/TimerProc is called in the context of the thread that owns the HWND, not the thread that is calling SetTimer(). This is NOT documented behavior, so do not rely on it! SetTimer()'s documentation clearly says the HWND "must be owned by the calling thread", as you stated in your question.

In any case, TTimer is a VCL component, and the VCL is inherently not thread-safe in general. Even though your TTimer code "works", it is not a good idea to access UI components outside of the main UI thread anyway, that is just bad code design. Stick with an alternative solution that is known to be thread-safe.

Edit: Thread safety is extremely difficult. I inserted AllocateHwnd() to replace self.handle according to mghie's comment.

Here is how I am planning to implement the UI notification + update. It's not any more complicated than the TTimer approach, and it doesn't have any thread safety issues that I know of. Different messages can be defined for different items that need to be updated.

If update notifications could be sent very rapidly, a variation on this theme is necessary to reduce the number of PostMessage calls. Also modifications are necessary if value cannot fit into WParam.

unit FormTestSync;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Forms, StdCtrls,
  Controls;

type
  TypeThreadTest = class(TThread)
    protected
      procedure Execute; override;
  end;


type
  TForm1 = class(TForm)
      Label1: TLabel;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    private
      const
        WM_UPDATE = WM_USER + 1;
      procedure OnMessage_Update(var message: TMessage);
    private
      m_thread: TypeThreadTest;
      m_hwndAlwaysThere: HWND;

    private
      procedure Notify(value: integer);
    public
  end;

var
  Form1: TForm1;


implementation
{$R *.dfm}


procedure TypeThreadTest.Execute;
begin
  while (not terminated) do begin
    //do work...
    form1.Notify(random(MaxInt));
  end;
end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  m_hwndAlwaysThere := AllocateHWnd(self.OnMessage_Update);
  m_thread := TypeThreadTest.Create();
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  m_thread.Terminate;
  m_thread.WaitFor;
  m_thread.Free;
  DeallocateHWnd(m_hwndAlwaysThere);
end;

procedure TForm1.Notify(value: integer);
begin
  //run on worker thread
  PostMessage(m_hwndAlwaysThere, WM_UPDATE, value, 0);
end;

procedure TForm1.OnMessage_Update(var message: TMessage);
begin
  //run on UI thread
  label1.Caption := IntToStr(message.WParam);
end;

end.
Related