我可以从非 UI 线程安全地启用 TTimer 吗?
Can I safely enable TTimer from a non-UI thread?
我在 Windows 10 上使用 Delphi XE7。
下面的代码我用了很久,刚看完SetTimer()
上的文档。简单地说,我正在从非 UI 线程设置计时器,但 Microsoft 的文档说它们只能在 UI 线程上设置。广泛的测试表明我的代码工作正常,但我不能相信我的系统与其他系统的行为相同,或者 Microsoft 文档是 100% 准确的。任何人都可以验证此代码是否可以吗?
Delphi 代码不会死锁,它几乎只是调用 SetTimer()
(我知道有竞争条件设置 TTimer.FEnabled
)。
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.
TTimer
正在主 UI 线程中构建,而 TForm
在其 DFM 资源中流式传输。 TTimer
的构造函数为计时器创建一个内部 HWND
来接收 WM_TIMER
消息。 HWND
因此属于主 UI 线程。
TForm.Notify()
正在将计时器的 Enabled
属性 设置为 true
,这将调用 SetTimer()
。 Notify()
在工作线程的上下文中被调用,而不是主 UI 线程。这个 不应该 工作,如 SetTimer()
's documentation 中所述。只有主 UI 线程应该能够启动计时器 运行,因为主 UI 线程拥有计时器的 HWND
.
TTimer.UpdateTimer()
,由计时器的 Enabled
、Interval
和 OnTimer
属性的设置程序在内部调用,如果出现以下情况,将引发 EOutOfResources
异常SetTimer()
失败。因此,在 TypeThreadTest.Execute()
中调用 form1.Notify()
不应 有效。在这种情况下,唯一不会调用 SetTimer()
的方法是:
Interval
是 0
Enabled
是 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
的线程的上下文中调用,而不是在拥有 HWND
的线程的上下文中调用呼叫 SetTimer()
。 这不是记录在案的行为,所以不要依赖它! SetTimer()
's documentation 清楚地说 HWND
“ 必须由调用线程拥有 ”,正如您在问题中所述。
无论如何,TTimer
是一个VCL组件,一般来说VCL天生就不是线程安全的。即使您的 TTimer
代码“有效”,在主 UI 线程之外访问 UI 组件也不是一个好主意,这只是糟糕的代码设计。坚持使用已知线程安全的替代解决方案。
编辑:线程安全极其困难。我根据 mghie 的评论插入 AllocateHwnd()
来替换 self.handle
。
以下是我计划实施 UI 通知 + 更新的方式。它并不比 TTimer 方法复杂,而且据我所知,它没有任何线程安全问题。可以为需要更新的不同项目定义不同的消息。
如果可以非常快速地发送更新通知,则需要对此主题进行变体以减少 PostMessage
调用次数。如果 value
不适合 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.
我在 Windows 10 上使用 Delphi XE7。
下面的代码我用了很久,刚看完SetTimer()
上的文档。简单地说,我正在从非 UI 线程设置计时器,但 Microsoft 的文档说它们只能在 UI 线程上设置。广泛的测试表明我的代码工作正常,但我不能相信我的系统与其他系统的行为相同,或者 Microsoft 文档是 100% 准确的。任何人都可以验证此代码是否可以吗?
Delphi 代码不会死锁,它几乎只是调用 SetTimer()
(我知道有竞争条件设置 TTimer.FEnabled
)。
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.
TTimer
正在主 UI 线程中构建,而 TForm
在其 DFM 资源中流式传输。 TTimer
的构造函数为计时器创建一个内部 HWND
来接收 WM_TIMER
消息。 HWND
因此属于主 UI 线程。
TForm.Notify()
正在将计时器的 Enabled
属性 设置为 true
,这将调用 SetTimer()
。 Notify()
在工作线程的上下文中被调用,而不是主 UI 线程。这个 不应该 工作,如 SetTimer()
's documentation 中所述。只有主 UI 线程应该能够启动计时器 运行,因为主 UI 线程拥有计时器的 HWND
.
TTimer.UpdateTimer()
,由计时器的 Enabled
、Interval
和 OnTimer
属性的设置程序在内部调用,如果出现以下情况,将引发 EOutOfResources
异常SetTimer()
失败。因此,在 TypeThreadTest.Execute()
中调用 form1.Notify()
不应 有效。在这种情况下,唯一不会调用 SetTimer()
的方法是:
Interval
是 0Enabled
是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
的线程的上下文中调用,而不是在拥有 HWND
的线程的上下文中调用呼叫 SetTimer()
。 这不是记录在案的行为,所以不要依赖它! SetTimer()
's documentation 清楚地说 HWND
“ 必须由调用线程拥有 ”,正如您在问题中所述。
无论如何,TTimer
是一个VCL组件,一般来说VCL天生就不是线程安全的。即使您的 TTimer
代码“有效”,在主 UI 线程之外访问 UI 组件也不是一个好主意,这只是糟糕的代码设计。坚持使用已知线程安全的替代解决方案。
编辑:线程安全极其困难。我根据 mghie 的评论插入 AllocateHwnd()
来替换 self.handle
。
以下是我计划实施 UI 通知 + 更新的方式。它并不比 TTimer 方法复杂,而且据我所知,它没有任何线程安全问题。可以为需要更新的不同项目定义不同的消息。
如果可以非常快速地发送更新通知,则需要对此主题进行变体以减少 PostMessage
调用次数。如果 value
不适合 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.