在 Delphi 中编程延迟的最佳方法是什么?
What is the best way to program a delay in Delphi?
我正在处理的 Delphi 应用程序必须延迟一秒,有时甚至两秒。我想使用最佳实践来对这种延迟进行编程。在 Whosebug 上阅读有关 Delphi 的 Sleep() 方法的条目时,我发现了这两条评论:
I live by this maxim: "If you feel the need to use Sleep(), you are doing it wrong." – Nick Hodges Mar 12 '12 at 1:36
@nick Indeed. My equivalent is "There are no problems for which Sleep is the solution." – David Heffernan Mar 12 '12 at 8:04
comments about Sleep()
为了响应避免调用 Sleep() 的建议,以及我对使用 Delphi 的 TTimer 和 TEvent 类 的理解,我编写了以下原型。我的问题是:
- 这是编程延迟的正确方法吗?
- 如果答案是肯定的,那么为什么这比调用 Sleep() 更好?
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
public
EventManager: TEvent;
end;
TDoSomething = class(TThread)
public
procedure Execute; override;
procedure Delay;
end;
var
Form1: TForm1;
Something: TDoSomething;
implementation
{$R *.dfm}
procedure TDoSomething.Execute;
var
i: integer;
begin
FreeOnTerminate := true;
Form1.Timer1.Interval := 2000; // 2 second interval for a 2 second delay
Form1.EventManager := TEvent.Create;
for i := 1 to 10 do
begin
Delay;
writeln(TimeToStr(GetTime));
end;
FreeAndNil(Form1.EventManager);
end;
procedure TDoSomething.Delay;
begin
// Use a TTimer in concert with an instance of TEvent to implement a delay.
Form1.Timer1.Enabled := true;
Form1.EventManager.ResetEvent;
Form1.EventManager.WaitFor(INFINITE);
Form1.Timer1.Enabled := false;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Something := TDoSomething.Create;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
// Time is up. End the delay.
EventManager.SetEvent;
end;
依次回答您的问题:
- 这是编程延迟的正确方法吗?
是(还有 "no" - 见下文)。
'proper way'根据具体要求和所解决的问题而有所不同。在这方面没有 普遍真理 ,任何告诉你其他情况的人都在试图向你推销东西(换句话说)。
在某些情况下,等待事件是正确的延迟机制。其他情况则不然。
- 如果答案是肯定的,那么为什么这比调用 Sleep() 更好?
见上:答案是是的。然而,这第二个问题根本没有意义,因为它假设 Sleep() 是 always 并且必然 从来不是正确的方法,因为在上面#1 的回答中解释,不一定是这样。
Sleep() 可能不是在 所有 场景中编程延迟的最佳或最合适的方法,但有些场景最实用,没有明显缺点。
为什么人们不睡觉
Sleep()是一个潜在的问题正是因为它是一个无条件的延迟,在特定的时间段过去之前不能被中断。替代延迟机制通常实现完全相同的事情,唯一的区别是存在一些替代机制来恢复执行,而不仅仅是时间的流逝。
等待事件延迟,直到事件发生(或被销毁)或特定的时间段已经过去。
等待互斥量会导致延迟,直到获得(或销毁)互斥量或经过特定时间段。
等等
换句话说:虽然一些延迟机制是可中断的。 Sleep() 不是。但是,如果您弄错了其他机制,仍然有可能引入重大问题,而且通常以更难以识别的方式出现。
在这种情况下 Event.WaitFor() 的问题
问题中的原型强调了使用 any 机制的潜在问题,如果代码的其余部分未以 与该特定方法兼容:
Form1.Timer1.Enabled := true;
Form1.EventManager.ResetEvent;
Form1.EventManager.WaitFor(INFINITE);
如果这段代码在主线程中执行,那么Timer1将永远不会发生。
问题中的原型是在一个线程中执行的,因此不会出现这个特殊问题,但值得探索其潜力,因为原型确实由于该线程的参与而引入了不同的问题。
通过在事件上的 WaitFor() 上指定 INFINITE 等待超时,您可以暂停线程的执行,直到该事件发生. TTimer 组件使用基于 windows 消息的计时器机制,其中 WM_TIMER 消息被提供给您的消息队列当计时器结束时。要出现 WM_TIMER 消息,您的应用程序必须正在处理其消息队列。
Windows 计时器也可以创建,它将在另一个线程上提供回调,这在这种(公认的人为的)情况下可能是更合适的方法。但是,这不是 VCL TTimer 组件提供的功能(至少从 XE4 开始,我注意到您使用的是 XE2)。
问题#1
如上所述,WM_TIMER 消息依赖于您的应用程序处理其消息队列。您指定了一个 2 秒的计时器,但如果您的应用程序进程正忙于做其他工作,则处理该消息可能需要超过 2 秒的时间。
这里值得一提的是 Sleep() 也存在一些不准确的地方 - 它确保线程暂停 至少 指定的时间段,它不完全保证指定的延迟。
问题#2
原型设计了一种机制,使用计时器和事件延迟 2 秒,以实现与简单调用 Sleep()[=113= 几乎完全相同的结果].
这与简单的 Sleep() 调用之间的唯一区别是,如果它正在等待的事件被销毁,您的线程也会恢复。
但是,在延迟之后会进行一些进一步处理的现实情况中,如果处理不当,这本身就是一个潜在的重大问题。在原型中,根本没有考虑到这种可能性。即使在这种简单的情况下,如果事件已被销毁,那么线程尝试禁用的 Timer1 也很可能被销毁。 访问冲突很可能在线程中发生,结果是当它试图禁用该计时器时。
开发者注意
武断地避免使用 Sleep() 不能替代正确理解所有线程同步机制(延迟只是其中之一)和操作系统本身的方式有效,以便可以根据每个场合的需要部署正确的技术。
事实上,就您的原型而言,Sleep() 可以说提供了 "better" 解决方案(如果可靠性是关键指标),因为它的简单性技术确保您的代码将在 2 秒后恢复,而不会落入陷阱,等待粗心的人使用过于复杂的(关于手头的问题)技术。
话虽如此,这个原型显然是一个人为的例子。
根据我的经验,很少 实际情况中 Sleep() 是最佳解决方案,尽管它通常是最简单的容易出错。但我永远不会说永远不会。
场景:
您想执行一些连续的操作,它们之间有一定的延迟。
Is this a proper way to program a delay?
我想说有更好的方法,见下文。
If the answer is yes, then why is this better than a call to Sleep()?
在主线程中休眠是个坏主意:请记住,windows 范例是事件驱动的,即根据操作执行任务,然后让系统处理接下来发生的事情。在线程中休眠也很糟糕,因为您可以拖延来自系统的重要消息(在关闭等情况下)。
您的选择是:
像状态机一样从主线程中的计时器处理您的操作。跟踪状态并在计时器事件触发时执行代表此特定状态的操作。这适用于每个计时器事件在短时间内完成的代码。
将动作行放在线程中。使用事件超时作为计时器,以避免通过睡眠调用冻结线程。这些类型的操作通常是 I/O 绑定的,您可以在其中调用具有内置超时的函数。在那些情况下,超时数用作自然延迟。这就是我所有通信库的构建方式。
后一种选择的示例:
procedure StartActions(const ShutdownEvent: TSimpleEvent);
begin
TThread.CreateAnonymousThread(
procedure
var
waitResult: TWaitResult;
i: Integer;
begin
i := 0;
repeat
if not Assigned(ShutdownEvent) then
break;
waitResult := ShutdownEvent.WaitFor(2000);
if (waitResult = wrTimeOut) then
begin
// Do your stuff
// case i of
// 0: ;
// 1: ;
// end;
Inc(i);
if (i = 10) then
break;
end
else
break; // Abort actions if process shutdown
until Application.Terminated;
end
).Start;
end;
称呼它:
var
SE: TSimpleEvent;
...
SE := TSimpleEvent.Create(Nil,False,False,'');
StartActions(SE);
并中止操作(在程序关闭或手动中止的情况下):
SE.SetEvent;
...
FreeAndNil(SE);
这将创建一个匿名线程,其中的时间由 TSimpleEvent
驱动。当动作线准备就绪时,线程将自行销毁。 "global" 事件对象可用于手动或在程序关闭期间中止操作。
这是一个在调用 ProcessMessages 时等待一段时间的过程(这是为了保持系统响应)。
procedure Delay(TickTime : Integer);
var
Past: longint;
begin
Past := GetTickCount;
repeat
application.ProcessMessages;
Until (GetTickCount - Past) >= longint(TickTime);
end;
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls, Vcl.ExtCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Memo1: TMemo;
Timer1: TTimer;
RichEdit1: TRichEdit;
Button1: TButton;
CheckBox1: TCheckBox;
procedure Delay(TickTime : Integer);
procedure Button1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
Past: longint;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
delay(180000);
beep;
end;
procedure TForm1.Delay(TickTime: Integer);
begin
Past := GetTickCount;
repeat
application.ProcessMessages;
Until (GetTickCount - Past) >= longint(TickTime);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if checkbox1.Checked=true then Past:=0;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
end;
end.
这是编写不阻塞主线程的睡眠实用程序的更简洁的方法。像这样使用它:
procedure SomeFunction(a:Integer);
begin
var b:= 2;
TSleep.Create(
5000, //milliseconds of sleep
procedure() begin
//this "anonymous procedure" runs after 5 seconds
//with full access to variables and input arguments
//like a & b.
end
); //TSleep frees itself via the FreeOnTerminate setting
end;
将 TSleep 实用程序添加到界面部分...
type
TCallback = reference to procedure();
TSleep = class(TThread)
protected
procedure Execute; override;
private
pMs: Integer;
pCallback: TCallback;
public
constructor Create(const aMs:Integer; aCallback:TCallback); virtual;
end;
...和实施部分。
{ TSleep }
constructor TSleep.Create(const aMs: Integer; aCallback: TCallback);
begin
inherited Create(false);//false means the Execute function runs immediately
FreeOnTerminate:= true;
NameThreadForDebugging('Sleep');
//save the input arguments for use by the new thread
pMs:= aMs;
pCallback:= aCallback;
end;
procedure TSleep.Execute;
begin
//this runs in separate thread
//wait
var pt:= nil;//pt must be a variable
MsgWaitForMultipleObjects(0, pt, false, pMs, 0);
//callback
Synchronize(
procedure begin
//this runs in main thread
pCallback();
end
);
end;
我正在处理的 Delphi 应用程序必须延迟一秒,有时甚至两秒。我想使用最佳实践来对这种延迟进行编程。在 Whosebug 上阅读有关 Delphi 的 Sleep() 方法的条目时,我发现了这两条评论:
I live by this maxim: "If you feel the need to use Sleep(), you are doing it wrong." – Nick Hodges Mar 12 '12 at 1:36
@nick Indeed. My equivalent is "There are no problems for which Sleep is the solution." – David Heffernan Mar 12 '12 at 8:04
comments about Sleep()
为了响应避免调用 Sleep() 的建议,以及我对使用 Delphi 的 TTimer 和 TEvent 类 的理解,我编写了以下原型。我的问题是:
- 这是编程延迟的正确方法吗?
- 如果答案是肯定的,那么为什么这比调用 Sleep() 更好?
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
public
EventManager: TEvent;
end;
TDoSomething = class(TThread)
public
procedure Execute; override;
procedure Delay;
end;
var
Form1: TForm1;
Something: TDoSomething;
implementation
{$R *.dfm}
procedure TDoSomething.Execute;
var
i: integer;
begin
FreeOnTerminate := true;
Form1.Timer1.Interval := 2000; // 2 second interval for a 2 second delay
Form1.EventManager := TEvent.Create;
for i := 1 to 10 do
begin
Delay;
writeln(TimeToStr(GetTime));
end;
FreeAndNil(Form1.EventManager);
end;
procedure TDoSomething.Delay;
begin
// Use a TTimer in concert with an instance of TEvent to implement a delay.
Form1.Timer1.Enabled := true;
Form1.EventManager.ResetEvent;
Form1.EventManager.WaitFor(INFINITE);
Form1.Timer1.Enabled := false;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Something := TDoSomething.Create;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
// Time is up. End the delay.
EventManager.SetEvent;
end;
依次回答您的问题:
- 这是编程延迟的正确方法吗?
是(还有 "no" - 见下文)。
'proper way'根据具体要求和所解决的问题而有所不同。在这方面没有 普遍真理 ,任何告诉你其他情况的人都在试图向你推销东西(换句话说)。
在某些情况下,等待事件是正确的延迟机制。其他情况则不然。
- 如果答案是肯定的,那么为什么这比调用 Sleep() 更好?
见上:答案是是的。然而,这第二个问题根本没有意义,因为它假设 Sleep() 是 always 并且必然 从来不是正确的方法,因为在上面#1 的回答中解释,不一定是这样。
Sleep() 可能不是在 所有 场景中编程延迟的最佳或最合适的方法,但有些场景最实用,没有明显缺点。
为什么人们不睡觉
Sleep()是一个潜在的问题正是因为它是一个无条件的延迟,在特定的时间段过去之前不能被中断。替代延迟机制通常实现完全相同的事情,唯一的区别是存在一些替代机制来恢复执行,而不仅仅是时间的流逝。
等待事件延迟,直到事件发生(或被销毁)或特定的时间段已经过去。
等待互斥量会导致延迟,直到获得(或销毁)互斥量或经过特定时间段。
等等
换句话说:虽然一些延迟机制是可中断的。 Sleep() 不是。但是,如果您弄错了其他机制,仍然有可能引入重大问题,而且通常以更难以识别的方式出现。
在这种情况下 Event.WaitFor() 的问题
问题中的原型强调了使用 any 机制的潜在问题,如果代码的其余部分未以 与该特定方法兼容:
Form1.Timer1.Enabled := true;
Form1.EventManager.ResetEvent;
Form1.EventManager.WaitFor(INFINITE);
如果这段代码在主线程中执行,那么Timer1将永远不会发生。
问题中的原型是在一个线程中执行的,因此不会出现这个特殊问题,但值得探索其潜力,因为原型确实由于该线程的参与而引入了不同的问题。
通过在事件上的 WaitFor() 上指定 INFINITE 等待超时,您可以暂停线程的执行,直到该事件发生. TTimer 组件使用基于 windows 消息的计时器机制,其中 WM_TIMER 消息被提供给您的消息队列当计时器结束时。要出现 WM_TIMER 消息,您的应用程序必须正在处理其消息队列。
Windows 计时器也可以创建,它将在另一个线程上提供回调,这在这种(公认的人为的)情况下可能是更合适的方法。但是,这不是 VCL TTimer 组件提供的功能(至少从 XE4 开始,我注意到您使用的是 XE2)。
问题#1
如上所述,WM_TIMER 消息依赖于您的应用程序处理其消息队列。您指定了一个 2 秒的计时器,但如果您的应用程序进程正忙于做其他工作,则处理该消息可能需要超过 2 秒的时间。
这里值得一提的是 Sleep() 也存在一些不准确的地方 - 它确保线程暂停 至少 指定的时间段,它不完全保证指定的延迟。
问题#2
原型设计了一种机制,使用计时器和事件延迟 2 秒,以实现与简单调用 Sleep()[=113= 几乎完全相同的结果].
这与简单的 Sleep() 调用之间的唯一区别是,如果它正在等待的事件被销毁,您的线程也会恢复。
但是,在延迟之后会进行一些进一步处理的现实情况中,如果处理不当,这本身就是一个潜在的重大问题。在原型中,根本没有考虑到这种可能性。即使在这种简单的情况下,如果事件已被销毁,那么线程尝试禁用的 Timer1 也很可能被销毁。 访问冲突很可能在线程中发生,结果是当它试图禁用该计时器时。
开发者注意
武断地避免使用 Sleep() 不能替代正确理解所有线程同步机制(延迟只是其中之一)和操作系统本身的方式有效,以便可以根据每个场合的需要部署正确的技术。
事实上,就您的原型而言,Sleep() 可以说提供了 "better" 解决方案(如果可靠性是关键指标),因为它的简单性技术确保您的代码将在 2 秒后恢复,而不会落入陷阱,等待粗心的人使用过于复杂的(关于手头的问题)技术。
话虽如此,这个原型显然是一个人为的例子。
根据我的经验,很少 实际情况中 Sleep() 是最佳解决方案,尽管它通常是最简单的容易出错。但我永远不会说永远不会。
场景: 您想执行一些连续的操作,它们之间有一定的延迟。
Is this a proper way to program a delay?
我想说有更好的方法,见下文。
If the answer is yes, then why is this better than a call to Sleep()?
在主线程中休眠是个坏主意:请记住,windows 范例是事件驱动的,即根据操作执行任务,然后让系统处理接下来发生的事情。在线程中休眠也很糟糕,因为您可以拖延来自系统的重要消息(在关闭等情况下)。
您的选择是:
像状态机一样从主线程中的计时器处理您的操作。跟踪状态并在计时器事件触发时执行代表此特定状态的操作。这适用于每个计时器事件在短时间内完成的代码。
将动作行放在线程中。使用事件超时作为计时器,以避免通过睡眠调用冻结线程。这些类型的操作通常是 I/O 绑定的,您可以在其中调用具有内置超时的函数。在那些情况下,超时数用作自然延迟。这就是我所有通信库的构建方式。
后一种选择的示例:
procedure StartActions(const ShutdownEvent: TSimpleEvent);
begin
TThread.CreateAnonymousThread(
procedure
var
waitResult: TWaitResult;
i: Integer;
begin
i := 0;
repeat
if not Assigned(ShutdownEvent) then
break;
waitResult := ShutdownEvent.WaitFor(2000);
if (waitResult = wrTimeOut) then
begin
// Do your stuff
// case i of
// 0: ;
// 1: ;
// end;
Inc(i);
if (i = 10) then
break;
end
else
break; // Abort actions if process shutdown
until Application.Terminated;
end
).Start;
end;
称呼它:
var
SE: TSimpleEvent;
...
SE := TSimpleEvent.Create(Nil,False,False,'');
StartActions(SE);
并中止操作(在程序关闭或手动中止的情况下):
SE.SetEvent;
...
FreeAndNil(SE);
这将创建一个匿名线程,其中的时间由 TSimpleEvent
驱动。当动作线准备就绪时,线程将自行销毁。 "global" 事件对象可用于手动或在程序关闭期间中止操作。
这是一个在调用 ProcessMessages 时等待一段时间的过程(这是为了保持系统响应)。
procedure Delay(TickTime : Integer);
var
Past: longint;
begin
Past := GetTickCount;
repeat
application.ProcessMessages;
Until (GetTickCount - Past) >= longint(TickTime);
end;
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls, Vcl.ExtCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Memo1: TMemo;
Timer1: TTimer;
RichEdit1: TRichEdit;
Button1: TButton;
CheckBox1: TCheckBox;
procedure Delay(TickTime : Integer);
procedure Button1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
Past: longint;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
delay(180000);
beep;
end;
procedure TForm1.Delay(TickTime: Integer);
begin
Past := GetTickCount;
repeat
application.ProcessMessages;
Until (GetTickCount - Past) >= longint(TickTime);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if checkbox1.Checked=true then Past:=0;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
end;
end.
这是编写不阻塞主线程的睡眠实用程序的更简洁的方法。像这样使用它:
procedure SomeFunction(a:Integer);
begin
var b:= 2;
TSleep.Create(
5000, //milliseconds of sleep
procedure() begin
//this "anonymous procedure" runs after 5 seconds
//with full access to variables and input arguments
//like a & b.
end
); //TSleep frees itself via the FreeOnTerminate setting
end;
将 TSleep 实用程序添加到界面部分...
type
TCallback = reference to procedure();
TSleep = class(TThread)
protected
procedure Execute; override;
private
pMs: Integer;
pCallback: TCallback;
public
constructor Create(const aMs:Integer; aCallback:TCallback); virtual;
end;
...和实施部分。
{ TSleep }
constructor TSleep.Create(const aMs: Integer; aCallback: TCallback);
begin
inherited Create(false);//false means the Execute function runs immediately
FreeOnTerminate:= true;
NameThreadForDebugging('Sleep');
//save the input arguments for use by the new thread
pMs:= aMs;
pCallback:= aCallback;
end;
procedure TSleep.Execute;
begin
//this runs in separate thread
//wait
var pt:= nil;//pt must be a variable
MsgWaitForMultipleObjects(0, pt, false, pMs, 0);
//callback
Synchronize(
procedure begin
//this runs in main thread
pCallback();
end
);
end;