首页 > 解决方案 > 我可以从非 UI 线程安全地启用 TTimer 吗?

问题描述

我在 Windows 10 上使用 Delphi XE7。

下面的代码我用了很长时间,只是阅读了SetTimer(). 简单地说,我是从非 UI 线程设置计时器,但微软的文档说它们应该只在 UI 线程上设置。广泛的测试表明我的代码运行良好,但我不能相信我的系统与其他系统的行为相同,或者 Microsoft 文档是 100% 准确的。任何人都可以验证此代码是否正常?

Delphi 代码不会死锁,它几乎只是调用SetTimer()(我知道有一个竞争条件设置TTimer.FEnabled)。

MSDN 文档说:

hWnd

类型:HWND

要与计时器关联的窗口句柄。此窗口必须由调用线程拥有。

我想要完成的是工作线程在做一些事情,并且在适当的时候,它们通知主线程必须更新 UI 的元素,并且主线程更新 UI。我知道如何使用TThread.Synchronize(),但在某些情况下可能会发生死锁。我可以PostMessage()从我的工作线程中使用并在 UI 线程中处理消息。

Delphi 中还有其他方法可以通知和更新 UI 线程吗?

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.

标签: multithreadingdelphiwinapi

解决方案


当流在其 DFM 资源中时,它TTimer正在主 UI 线程中构建。TFormTTimer构造函数为定时器创建一个内部HWND来接收WM_TIMER消息。因此,它HWND 归主 UI 线程所有。

TForm.Notify()正在将计时器的Enabled属性设置为true,这将调用SetTimer()Notify()在工作线程的上下文中调用,而不是在主 UI 线程中调用。如's documentation所述,这不应该起作用。只有主 UI 线程应该能够启动计时器运行,因为主 UI 线程拥有计时器的.SetTimer()HWND

TTimer.UpdateTimer(), 由定时器的Enabled,IntervalOnTimer属性的设置器内部调用,EOutOfResources如果SetTimer()失败将引发异常。form1.Notify()所以,打电话TypeThreadTest.Execute() 不应该工作。SetTimer()在这种情况下不会调用的唯一方法是:

  • Interval是 0
  • Enabledfalse
  • OnTimer未分配

否则,您的工作线程应该崩溃。

正如您所指出的,当您的工作线程想要通知主 UI 线程做某事时,它可以交替使用TThread.Synchronize()(or TThread.Queue()) 或PostMessage()(or )。SendMessage()这些是可行和首选的解决方案。就个人而言,我会选择TThread.Queue(),例如:

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.

如果您想TTimer改用这项工作,您可以做的只是在主 UI 线程中启用计时器并保持启用状态,然后同步对计时器定期访问的数据的访问。那将是非常安全的,例如:

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.

更新:

我做了一个快速测试。当SetTimer()被另一个线程拥有的非NULL调用时HWND,在Windows XP,7和10(我没有测试Vista或8)上果然SetTimer() 成功,并且WM_TIMER/TimerProc在拥有的线程的上下文中调用HWND,而不是调用的线程SetTimer()这不是记录在案的行为,所以不要依赖它! SetTimer()的文档清楚地表明HWND必须由调用线程拥有”,正如您在问题中所述。

在任何情况下,TTimer它都是一个 VCL 组件,而 VCL 通常本身就不是线程安全的。即使您的TTimer代码“有效”,无论如何访问主 UI 线程之外的 UI 组件也不是一个好主意,这只是糟糕的代码设计。坚持使用已知是线程安全的替代解决方案。


推荐阅读