在 Delphi 中编程延迟的最佳方法是啥?
Posted
技术标签:
【中文标题】在 Delphi 中编程延迟的最佳方法是啥?【英文标题】:What is the best way to program a delay in Delphi?在 Delphi 中编程延迟的最佳方法是什么? 【发布时间】:2015-04-24 04:57:00 【问题描述】:我正在开发的 Delphi 应用程序必须延迟一秒,有时甚至是两秒。我想使用最佳实践来编程这个延迟。在阅读有关 *** 上 Delphi 的 Sleep() 方法的条目时,我发现了这两个 cmets:
我信奉这样的格言:“如果你觉得需要使用 Sleep(),那你就做错了。” – 尼克霍奇斯 2012 年 3 月 12 日 1:36
@nick 确实如此。我的等价物是“没有问题可以通过睡眠来解决。” – 大卫赫弗南 2012 年 3 月 12 日 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;
【问题讨论】:
请解释什么必须延迟。 - 顺便说一句,“最佳方式”闻起来“主要基于意见” 应用程序与外部设备连接。外部设备通常会告知应用程序待机并等待一秒或多秒,然后才能获得可供应用程序使用的数据。 是推还是拉?当数据准备好(推送)时,外部设备会通知应用程序,还是必须一遍又一遍地询问,直到数据可用(拉取)?无论如何,您应该始终使用事件来等待。推无限超时或拉几毫秒 不必要的复杂,您可以完全删除计时器并等待事件 2000 毫秒而不是无限期。让它自己超时,而不是在定时器间隔后戳它。然后归结为WaitForMultipleObjectsEx
是否优于Sleep
。我个人的看法,我没觉得 Sleep here 有什么问题。
我在工业自动化中一直使用睡眠 - 总是在后台线程上,但正是出于上述原因。有时,您必须等待现实世界中的某些事物不会或不会向您传达它们的准备情况,但它们仍然会以非常有规律和及时的方式准备好。它不一定是优雅的,但它是合乎逻辑的,可以理解的,并且有效。不是理想主义幻想中的“最佳”解决方案,而是务实现实主义世界中的“最佳”解决方案。
【参考方案1】:
依次回答你的问题:
-
这是编程延迟的正确方法吗?
是(但也“否” - 见下文)。
“适当的方法”根据具体要求和要解决的问题而有所不同。在这方面没有普遍真理,任何告诉您其他情况的人都试图向您推销一些东西(意译)。
在某些情况下,等待事件是适当的延迟机制。在其他情况下不是。
-
如果答案是肯定的,那为什么这比调用 Sleep() 更好?
见上文:答案是是的。然而,这第二个问题根本没有意义,因为它假设 Sleep() 总是而且必然 从来不是正确的方式,正如对 # 的回答中所解释的那样上面的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() 几乎完全相同的结果。
这与简单的 Sleep() 调用之间的唯一区别是,如果它正在等待的事件被破坏,您的线程也会恢复。
但是,在实际情况下,延迟之后会进行一些进一步的处理,如果处理不当,这本身就是一个潜在的重大问题。在原型中,根本没有考虑到这种可能性。即使在这种简单的情况下,如果事件已被销毁,那么线程尝试禁用的 Timer1 也很可能也是如此。因此,当线程尝试禁用该计时器时,可能会在线程中发生访问冲突。
警告开发者
教条地避免使用 Sleep() 并不能替代正确理解所有线程同步机制(延迟只是其中之一)和操作系统本身的工作方式,以便可以根据每个场合的需要部署正确的技术。
事实上,就您的原型而言,Sleep() 可以说是“更好”的解决方案(如果可靠性是关键指标),因为该技术的简单性可确保您的代码能够恢复2 秒后不会陷入使用过于复杂(就手头的问题而言)技术的粗心大意的陷阱。
话虽如此,这个原型显然是一个人为的例子。
根据我的经验,很少 实际情况中 Sleep() 是最佳解决方案,尽管它通常是最简单的最不容易出错的解决方案。但我永远不会说永远。
【讨论】:
TDoSomething.Delay 从线程调用。这意味着线程进入睡眠状态,而主线程仍然可以处理消息。 有时我们说永远不要强调。作为鼓励人们停下来倾听的一种方式。在这个问题的场景中,使用Sleep
有时会意味着关机时的延迟。这可能是可以接受的。这当然是可以避免的。
@David:为什么有人说“从不”的微妙之处经常被听者忽略。更准确地说,当更完整、更准确的建议被认为太复杂以至于普通人无法理解时,我会说“从不”。
@LU RD - 是的,我错过了 TDoSomething 是 TThread 派生类。感谢您的提醒。
或者评论的可用空间排除了完整的解释【参考方案2】:
场景: 你想执行一些连续的动作,它们之间有一定的延迟。
这是编程延迟的正确方法吗?
我想说有更好的方法,见下文。
如果答案是肯定的,那为什么这比调用 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
驱动。当行动线准备好时,线程将自毁。 “全局”事件对象可用于手动中止操作或在程序关闭期间中止操作。
【讨论】:
这是一个非常有趣的方法+1 语句“使用事件超时作为计时器来避免因睡眠调用而冻结线程” 是对的,实现不是。你就是这样做的。而且由于 匿名 没有办法终止线程 从外部 (即使是这样,在最坏的情况下,您可能会在WaitFor
行上等待 2 秒)。
@TLama,是的,你是对的。要有序地关闭匿名线程,必须暴露信号事件。更新了答案。
从主线程发出信号后直接释放事件是否安全?这不会引入竞争条件吗?
@LURD 感谢您解决我对该更新的担忧。我将删除我的 cmets。【参考方案3】:
这是一个在调用 ProcessMessages 时等待一段时间的过程(这是为了保持系统响应)。
procedure Delay(TickTime : Integer);
var
Past: longint;
begin
Past := GetTickCount;
repeat
application.ProcessMessages;
Until (GetTickCount - Past) >= longint(TickTime);
end;
【讨论】:
请不要只发布代码块 - 解释它的作用以及它如何解决问题。【参考方案4】: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.
【讨论】:
嘿@novice90,欢迎来到SO。请查看how to write a good answer 上的以下链接。如果您提供一些额外的解释而不是仅仅粘贴代码,它会有所帮助。 请添加一些解释。以上是关于在 Delphi 中编程延迟的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章
使用 APScheduler 在 python 中进行并行编程的最佳方法是啥?
在 Delphi 2009 中显示 PDF 文件的最佳方法是啥[关闭]