在 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 类 的理解,我编写了以下原型。我的问题是:

  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;

依次回答您的问题:

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

是(还有 "no" - 见下文)。

'proper way'根据具体要求和所解决的问题而有所不同。在这方面没有 普遍真理 ,任何告诉你其他情况的人都在试图向你推销东西(换句话说)。

在某些情况下,等待事件是正确的延迟机制。其他情况则不然。

  1. 如果答案是肯定的,那么为什么这比调用 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;