multithreading - 我可以从非 UI 线程安全地启用 TTimer 吗?

标签 multithreading delphi winapi

我在 Windows 10 上使用 Delphi XE7。

我已经使用下面的代码很长时间了,只是阅读了SetTimer()上的文档。简单地说,我是从非 UI 线程设置计时器,但 Microsoft 的文档说它们只能在 UI 线程上设置。广泛的测试表明我的代码工作正常,但我不能相信我的系统的行为与其他系统相同,也不相信 Microsoft 文档 100% 准确。谁能验证一下这段代码是否OK?

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

MSDN documentation说:

hWnd

Type: HWND

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

我想要完成的是工作线程做一些事情,并且在适当的时候,它们通知主线程必须更新 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.

最佳答案

TForm 在其 DFM 资源中流动时,TTimer 正在主 UI 线程中构建。 TTimer 的构造函数为计时器创建一个内部 HWND 来接收 WM_TIMER 消息。因此,该 HWND 由主 UI 线程拥有。

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

TTimer.UpdateTimer(),由定时器的EnabledIntervalOnTimer的setter在内部调用code> 属性,如果 SetTimer() 失败,将引发 EOutOfResources 异常。因此,在 TypeThreadTest.Execute() 中调用 form1.Notify() 不应该起作用。在这种情况下,SetTimer() 不会被调用的唯一方法是:

  • 间隔为0
  • 已启用false
  • OnTimer 未分配

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

正如您所注意到的,您的工作线程也可以使用 TThread.Synchronize() (或 TThread.Queue())或 PostMessage() (或 SendMessage()),当它想要通知主 UI 线程做某事时。这些都是可行且优选的解决方案。就我个人而言,我会选择 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.

更新:

我做了一个快速测试。当使用另一个线程拥有的非 NULL HWND 调用 SetTimer() 时,在 Windows XP、7 和 10 上确实如此(我没有测试 Vista 或 8) ), SetTimer() 成功,并且在拥有该函数的线程上下文中调用 WM_TIMER/TimerProc HWND,而不是调用 SetTimer() 的线程。 这不是有记录的行为,所以不要依赖它! SetTimer()'s documentation正如您在问题中所述,明确表示 HWND必须由调用线程拥有”。

无论如何,TTimer是一个VCL组件,并且VCL本质上一般来说不是线程安全的。即使您的 TTimer 代码“有效”,但无论如何访问主 UI 线程之外的 UI 组件都不是一个好主意,这只是糟糕的代码设计。坚持使用已知线程安全的替代解决方案。

关于multithreading - 我可以从非 UI 线程安全地启用 TTimer 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67426727/

相关文章:

.net - 如何找到ManualResetEvent处于什么状态?

java - ImageIO.read 在多线程执行中抛出异常

delphi - 是否有类似StrToCurr的函数可以处理数千个分隔符?

delphi - WinInet+SSL : Can't make abbreviated SSL handshake

c - 调整权限 SE_DEBUG_NAME

c - Win32 剪贴板和 alpha channel 图像

具有可更新 JProgressBar 的 Java Swing 线程

c++ - ZeroMQ 服务器如何维护与所有客户端的连接?

Delphi 10 西雅图 IDE 问题 : no hint after ( on a function/proc

c++ - 如何使用 Win32APi c++ 将图像设置为按钮?