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

Mik*_*ski 14 delphi events sleep timer delphi-xe2

我正在处理的Delphi应用程序必须延迟一秒或有时两秒钟.我想使用最佳实践来编制此延迟.在阅读关于stackoverflow的Delphi的Sleep()方法的条目时,我发现了这两条注释:

我遵循这句格言:"如果你觉得需要使用Sleep(),那你做错了." - Nick Hodges 12年12月12日1:36

@nick确实.我的等价物是"没有问题,睡眠是解决方案." - David Heffernan 12年12月12日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;
Run Code Online (Sandbox Code Playgroud)

Del*_*ics 16

轮流提出问题:

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

是(但也是"否" - 见下文).

"正确的方式"根据具体要求和要解决的问题而变化.对此没有普遍的真相,任何告诉你的人都试图向你推销一些东西(用来解释).

在某些情况下,等待事件是适当的延迟机制.在其他情况下没有.

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

见上文:答案肯定的.然而,这第二个问题根本就没有,因为它假定有意义的睡眠()始终和必然永远正确的方式,正如在回答#1以上解释的,并不一定是这样.

Sleep()可能不是在所有场景中编程延迟的最佳或最合适的方法,但有些情况下它是最实用的并且没有明显的缺点.

为什么人们避免睡觉()

Sleep()是一个潜在的问题,因为它是一个无条件的延迟,在特定的时间段过去之前不能中断.替代延迟机制通常实现完全相同的事情,唯一的区别是存在一些替代机制来恢复执行,而不仅仅是时间的流逝.

等待事件延迟直到事件发生(或被销毁)特定时间段过去.

等待互斥锁会导致延迟,直到获取(或被破坏)互斥锁经过特定时间段.

等等

换句话说:虽然一些延迟机制是可中断的. 睡眠()不是.但是,如果你得到其他错误的机制,仍然有可能引入重大问题,并且往往以一种更难以识别的方式.

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

问题中的原型强调了使用任何暂停代码执行的机制的潜在问题,如果代码的其余部分没有以与特定方法兼容的方式实现:

 Form1.Timer1.Enabled := true;
 Form1.EventManager.ResetEvent;
 Form1.EventManager.WaitFor(INFINITE);
Run Code Online (Sandbox Code Playgroud)

如果在主线程中执行此代码,则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()是最佳解决方案的实际情况很少,尽管它通常是最简单的最不容易出错的.但我永远不会说永远不会.


LU *_* RD 11

场景:您希望执行一些连续的操作,并在它们之间有一定的延迟.

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

我想说有更好的方法,见下文.

如果答案是肯定的,那么为什么这比调用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;
Run Code Online (Sandbox Code Playgroud)

叫它:

var
  SE: TSimpleEvent;
...
SE := TSimpleEvent.Create(Nil,False,False,'');
StartActions(SE);
Run Code Online (Sandbox Code Playgroud)

并中止操作(在程序关闭或手动中止的情况下):

SE.SetEvent;
...
FreeAndNil(SE);
Run Code Online (Sandbox Code Playgroud)

这将创建一个匿名线程,其中时间由a驱动TSimpleEvent.当行动就绪时,线程将被自行销毁."全局"事件对象可用于手动或在程序关闭期间中止操作.