在Delphi中编写延迟的最佳方法是什么?

19
我正在开发的Delphi应用程序需要延迟一秒或两秒。我想使用最佳实践来编程这个延迟。在阅读了关于Delphi的Sleep()方法的条目后,我发现了以下两个评论:“我坚持这样的信念:“如果你感觉需要使用Sleep(),那么你做错了。”-Nick Hodges Mar 12 '12 at 1:36" "确实如此。我的等效物是“没有问题可以通过Sleep解决。”-David Heffernan Mar 12 '12 at 8:04" 有关Sleep()的评论 针对避免调用Sleep()的建议以及我对使用Delphi的TTimer和TEvent类的理解,我编写了以下原型。我的问题是:
1. 这是一种正确的编程延迟的方式吗? 2. 如果答案是肯定的,那么为什么这比调用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;

请解释需要延迟的是什么。另外,“最佳方式”听起来“主观意见为主”。 - Sir Rufo
该应用程序与外部设备进行接口连接。该应用程序经常被外部设备告知待机并等待一秒或多秒钟,直到有数据可供应用程序使用。 - Mike Jablonski
1
这是一个推送还是拉取的过程?外部设备在数据准备好时是否会通知应用程序(推送),或者您必须一遍又一遍地询问,直到数据可用(拉取)?无论如何,您应该始终使用事件来等待。使用推送可以实现无限超时,而使用拉取则需要等待数毫秒。 - Sir Rufo
1
过于复杂了,你可以完全移除计时器,并在事件上等待2000毫秒而不是无限期地等待。让它自己超时,而不是在计时器间隔后戳它。然后就看WaitForMultipleObjectsEx是否比Sleep更好了。我的个人观点是,在这里使用Sleep没有任何问题。 - Sertac Akyuz
5
我在工业自动化中经常使用睡眠功能 - 总是在后台线程上运行,但正是出于上述原因。有时,你必须等待现实世界中的事物,它们不会或不愿意向你通报它们的准备情况,但仍然会以非常规律和及时的方式准备好。这不一定优雅,但它是合乎逻辑、可理解和有效的。在理想主义幻想中可能不是“最好”的解决方案,但在现实主义者的世界中却是“最好”的解决方案。 - J...
显示剩余3条评论
5个回答

19

针对您的问题,我的回答如下:

  1. 这是一种适当的编写延迟的方法吗?

是(但也不是-请见下文)。

“适当的方式”因具体要求和问题而异。在此方面,没有任何“普遍真理”,任何声称有此类真理的人都试图向您销售某些东西(引用的话)。有时等待事件是合适的延迟机制,而其他情况则不然。

  1. 如果答案是肯定的,那么为什么这比调用Sleep()更好?

请参见上面的答案:答案是肯定的。但是,第二个问题并没有意义,因为它假设Sleep()总是必要的延迟机制,这并不一定是正确的,就像以上回答中所解释的那样。

Sleep()可能不是在所有情况下编写延迟的最佳或最合适方式,但有些情况下它是最实用的,并且没有显着的缺点。

为什么人们避免使用Sleep()

Sleep()是一个潜在的问题,因为它是一种无条件的延迟,直到经过特定的时间为止,无法被中断。其他替代延迟机制通常完全达到了同样的效果,唯一的区别在于存在某种替代机制来恢复执行,而不仅仅是时间的流逝。

等待事件会延迟直到事件发生(或被销毁)或经过特定的时间。

等待互斥锁会导致延迟直到获取互斥锁(或被销毁)或经过特定的时间。

等等。

换句话说:虽然有些延迟机制是可中断的,但Sleep()不是。但如果您选择了其他机制并使用不当,仍然可能引入重大问题,而这些问题通常很难识别。

在这种情况下使用Event.WaitFor()存在的问题

问题中的原型突显了使用任何暂停代码执行的机制可能存在的一个问题,即如果其余代码的实现方式与该特定方法不兼容,则会出现问题:

 Form1.Timer1.Enabled := true;
 Form1.EventManager.ResetEvent;
 Form1.EventManager.WaitFor(INFINITE);
如果这段代码在主线程中执行,那么计时器1将永远不会发生。 问题的原型在一个线程中执行,因此这个特定问题不会出现,但是值得探讨潜在问题,因为该原型由于涉及此线程而引入了不同的问题。
在事件的WaitFor()上指定INFINITE等待超时时间,可以暂停线程执行直到事件发生。 TTimer组件使用基于Windows消息的计时器机制,在计时器已经过时时向您的消息队列提供WM_TIMER消息。要出现 WM_TIMER 消息,应用程序必须处理其消息队列。
Windows计时器也可以创建以在另一个线程上调用回调函数的方式提供,这可能是这种情况下(尽管人为的)更合适的方法。 但是,这不是VCL TTimer组件所提供的功能(至少在XE4中是如此,而且我注意到您正在使用XE2)。
问题#1
如上所述,WM_TIMER消息依赖于您的应用程序处理其消息队列。 您已指定2秒计时器,但是如果应用程序进程正在忙于其他工作,则可能需要比2秒更长的时间才能处理该消息。
值得一提的是,Sleep()也具有一定的不准确性-它确保线程暂停至少指定的时间,但不保证确切的延迟。
问题#2
原型通过计时器和事件构建了一种机制,以实现几乎可以通过简单调用Sleep()来实现相同的结果。
这与简单的Sleep()调用之间唯一的区别在于,如果线程正在等待的事件被销毁,则线程也将恢复。
但是,在实际情况下,如果延迟后还需要进行一些进一步的处理,则这本身就是一个潜在的问题,如果处理不正确,则会出现问题。 在原型中,这种可能性根本没有得到处理。 即使在这种简单情况下,如果事件已被销毁,则线程尝试禁用的计时器1也很可能已被销毁。 当线程尝试禁用该计时器时,将导致Access Violation错误。
开发者注意事项
死板地避免使用Sleep()并不能替代完全理解所有线程同步机制(其中延迟只是其一)和操作系统本身工作方式的必要性,以便针对每个场合采用正确的技术。
实际上,在您的原型中,Sleep()提供了“更好”的解决方案(如果可靠性是关键度量),因为该技术的简单性确保您的代码将在2秒后恢复,而不会陷入等待令人困惑的陷阱(相对于手头问题而言)的过于复杂的技术。
话虽如此,这个原型显然是一个人为的例子。
根据我的经验, Sleep()是最优解的实际情况非常少见,尽管它通常是最简单且最少出错的。 但我永远不会说永远。

TDoSomething.Delay 是从线程中调用的。这意味着该线程被挂起,而主线程仍然可以处理消息。 - LU RD
1
有时我们说“永远不要”是为了强调。这是鼓励人们停下来倾听的一种方式。在这个问题的情况下,使用Sleep有时会导致关闭时的延迟。这可能是可以接受的。当然也可以避免。 - David Heffernan
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Deltics
1
或者评论区可用的空间不足以提供完整的解释。 - David Heffernan
然而,在这里睡觉是没有用的,所以也许最好的办法是坚持自己的想法。 - David Heffernan
显示剩余2条评论

12

情境: 您希望执行一些连续的操作,并在它们之间设置一定的延迟。

这是编程延迟的正确方法吗?

我会说有更好的方法,见下文。

如果答案是肯定的,那么为什么这比调用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 - SilverWarior
这个语句“使用事件超时作为计时器,避免使用休眠调用冻结线程”是正确的,但实现方式不对。你正在做这件事。由于匿名性,没有办法从外部终止线程(即使有,最坏情况下你可能会在WaitFor行上等待2秒钟)。 - TLama
@TLama,是的,你说得对。为了有序地关闭匿名线程,必须公开信号事件。已更新答案。 - LU RD
直接从主线程发出信号后立即释放事件是安全的吗?这样做不会引入竞争条件吗? - Jens Mühlenhoff
@LURD 感谢你解决了我的关注的问题。我会移除我的评论。 - David Heffernan
@LU RD,感谢您回答我的问题。您的答案很有帮助和趣味性。我会尝试您提供的替代方案。 - Mike Jablonski

1
这里有一种更加清晰的编写睡眠工具的方式,它不会阻塞主线程。使用方法如下:
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;

0
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。请查看以下链接,了解如何编写好的答案:https://stackoverflow.com/help/answering。如果您提供一些额外的解释而不仅仅是粘贴代码,这会很有帮助。 - TheWhiteRabbit
请添加一些解释。 - Adrita Sharma

-1
这是一个等待一定时间的过程,同时调用ProcessMessages(这是为了保持系统响应)。
procedure Delay(TickTime : Integer);
 var
 Past: longint;
 begin
 Past := GetTickCount;
 repeat
 application.ProcessMessages;
 Until (GetTickCount - Past) >= longint(TickTime);
end;

6
请勿只发布代码块-请解释它的功能以及如何解决问题。 - CertainPerformance

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接