multithreading - 我可以从非 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.
解决方案
当流在其 DFM 资源中时,它TTimer
正在主 UI 线程中构建。TForm
的TTimer
构造函数为定时器创建一个内部HWND
来接收WM_TIMER
消息。因此,它HWND
归主 UI 线程所有。
TForm.Notify()
正在将计时器的Enabled
属性设置为true
,这将调用SetTimer()
。 Notify()
在工作线程的上下文中调用,而不是在主 UI 线程中调用。如's documentation所述,这不应该起作用。只有主 UI 线程应该能够启动计时器运行,因为主 UI 线程拥有计时器的.SetTimer()
HWND
TTimer.UpdateTimer()
, 由定时器的Enabled
,Interval
和OnTimer
属性的设置器内部调用,EOutOfResources
如果SetTimer()
失败将引发异常。form1.Notify()
所以,打电话TypeThreadTest.Execute()
不应该工作。SetTimer()
在这种情况下不会调用的唯一方法是:
Interval
是 0Enabled
是false
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 组件也不是一个好主意,这只是糟糕的代码设计。坚持使用已知是线程安全的替代解决方案。
推荐阅读
- javascript - 如何获取属性值的编号?
- javascript - 如何根据 JavaScript 中的键值正确设置输入?
- python - 选择案例的 Python 等效项 (CDec(variable)
- swift - xcode 10.1 场景套件动画 - 资源 - 错误?
- android - 数据库在项目中丢失 - 不在资产中
- python - Matplotlib 图形线不显示?
- bash - 为什么interact不能保持ssh登录状态?
- java - 与 Log4j2 相关的查询 - 执行延迟
- sql-server - 连接到其他表的 INSERT INTO 语句
- sql - 查询将计算文本值并返回具有最低计数值(min)的行?