Каков наилучший способ запрограммировать задержку в Delphi?

Приложение Delphi, над которым я работаю, должно задерживаться на один, а иногда на два, второй (и). Я хочу запрограммировать эту задержку, используя лучшие практики. При чтении записей о методе Delphi Sleep() в stackoverflow я нашел эти два комментария:

Я живу по этой максиме: "Если вы чувствуете необходимость использовать Sleep(), вы делаете это неправильно". - Ник Ходжес Мар 12 '12 в 1:36

@nick Действительно. Мой эквивалент: "Нет проблем, для которых Сон - это решение". - Дэвид Хеффернан Мар 12 '12 в 8:04

комментарии о 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;

Ответ 1

Принимая во внимание ваши вопросы:

  • Это правильный способ запрограммировать задержку?

Да (но также "нет" - см. ниже).

"Правильный способ" варьируется в зависимости от конкретных требований и решаемой проблемы. На этом нет Универсальной Правды, и любой, кто говорит вам иначе, пытается продать вам что-то (перефразировать).

В некоторых случаях ожидание события является надлежащим механизмом задержки. В других случаях нет.

  1. Если да, то почему это лучше, чем вызов Sleep()?

См. выше: ответ да. Однако этот второй вопрос просто не имеет смысла, поскольку он предполагает, что Sleep() всегда и по необходимости никогда не будет надлежащим образом, который, как объясняется в ответе на № 1 выше, не обязательно случай.

Сон() не может быть лучшим или наиболее подходящим способом программирования задержки во всех сценариях, но есть сценарии, где это наиболее практично и не имеет существенных недостатков.

Почему люди избегают сна() ing

Сон() является потенциальной проблемой именно потому, что это безусловная задержка, которая не может быть прервана до истечения определенного периода времени. Альтернативные механизмы задержки обычно достигают точно той же самой вещи, с той лишь разницей, что существует альтернативный механизм возобновления исполнения, отличный от простого времени.

Ожидание события задерживается до тех пор, пока событие не произойдет (или не будет уничтожено) или. Прошел определенный период времени.

Ожидание мьютекса вызывает задержку до тех пор, пока мьютекс не будет получен (или не будет уничтожен) или. Прошел определенный период времени.

и др.

Иными словами: в то время как некоторые механизмы задержки прерываются. Сон() - нет. Но если вы ошиблись в других механизмах, все еще есть потенциал для внесения значительных проблем и часто таким образом, что определить их гораздо труднее.

Проблемы с Event.WaitFor() в этом случае

Прототип в вопросе подчеркивает потенциальную проблему использования любого механизма, который приостанавливает выполнение вашего кода, если остальная часть этого кода не реализована способом, совместимым с этим конкретным подходом:

 Form1.Timer1.Enabled := true;
 Form1.EventManager.ResetEvent;
 Form1.EventManager.WaitFor(INFINITE);

Если этот код выполняется в основном потоке, тогда Timer1 никогда не будет.

Прототип в вопросе выполняет это в потоке, поэтому эта конкретная проблема не возникает, но стоит изучить потенциал, так как прототип представляет собой другую проблему в результате участия этого потока.

Указав INFINITE время ожидания ожидания на WaitFor() в событии, вы приостанавливаете выполнение потока до тех пор, пока это событие не произойдет. Компонент TTimer использует механизм таймера, основанный на сообщениях Windows, в котором сообщение WM_TIMER отправляется в очередь сообщений по истечении времени таймера. Для сообщения WM_TIMER ваше приложение должно обрабатывать очередь сообщений.

Также могут быть созданы таймеры Windows, которые будут обеспечивать обратный вызов в другом потоке, что может быть более подходящим подходом в этом (предположительно искусственном) случае. Однако это не возможность, предлагаемая компонентом VCL TTimer (как минимум на XE4, и я отмечаю, что вы используете XE2).

Проблема № 1

Как отмечалось выше, сообщения WM_TIMER полагаются на ваше приложение, обрабатывающее его очередь сообщений. Вы указали 2-секундный таймер, но если ваш прикладной процесс занят другой работой, для обработки этого сообщения может потребоваться гораздо больше времени, чем 2 секунды.

Здесь стоит упомянуть, что Сон() также подвержен некоторой неточности - он гарантирует, что поток приостановлен в течение как минимум указанного периода времени, он не гарантирует точно указанную задержку.

Проблема № 2

Прототип изобретает механизм задержки в течение 2 секунд с использованием таймера и события для достижения почти точно такого же результата, который мог быть достигнут при простом вызове Сон().

Единственное различие между этим и простым вызовом Sleep() заключается в том, что ваш поток также возобновится, если ожидаемое событие будет уничтожено.

Однако, в реальной ситуации, когда некоторая дальнейшая обработка следует за задержкой, это само по себе является потенциально важной проблемой, если ее неправильно обрабатывать. В прототипе эта возможность вообще не обслуживается. Даже в этом простом случае, скорее всего, если событие было уничтожено, то также имеет Timer1, который поток пытается отключить. Нарушение доступа, скорее всего, произойдет в потоке в результате, когда он попытается отключить этот таймер.

Разработчик Caveat

Догматически избегая использования Sleep() не подлежит должному пониманию всех механизмов синхронизации потоков (из которых задержки - всего одна) и способа работы самой операционной системы, чтобы правильная техника может быть развернута по мере необходимости.

Фактически, в случае вашего прототипа Сон() предоставляет, возможно, "лучшее" решение (если надежность является ключевой метрикой), поскольку простота этого метода гарантирует, что ваш код вернется через 2 секунды, не попадая в ловушки, которые ждут неосторожного с чрезмерно сложными (по отношению к проблеме) методами.

Сказав это, этот прототип явно надуманный пример.

По моему опыту очень мало практических ситуаций, когда Сон() является оптимальным решением, хотя он часто является самым простым наименее подверженным ошибкам. Но я никогда не скажу никогда.

Ответ 2

Сценарий: Вы хотите выполнить некоторые последовательные действия с определенной задержкой между ними.

Это правильный способ запрограммировать задержку?

Я бы сказал, что есть лучшие способы, см. ниже.

Если да, то почему это лучше, чем вызов Sleep()?

Сон в основном потоке - плохая идея: помните, что парадигма окон управляется событиями, т.е. ваша задача основана на действии, а затем позволяет системе обрабатывать то, что происходит дальше. Сон в потоке также плох, так как вы можете останавливать важные сообщения из системы (в случае выключения и т.д.).

Ваши варианты:

  • Управляйте своими действиями с помощью таймера в основном потоке, таком как конечный автомат. Следите за состоянием и просто выполняйте действие, которое представляет это конкретное состояние при срабатывании события таймера. Это работает для кода, который заканчивается за короткое время для каждого события таймера.

  • Поместите строку действий в поток. Используйте тайм-аут события как таймер, чтобы избежать замораживания потока с помощью вызовов сна. Часто эти типы действий связаны с привязкой ввода/вывода, где вы вызываете функции со встроенным таймаутом. В этих случаях номер тайм-аута служит естественной задержкой. Вот как все мои библиотеки связи построены.

Пример последней альтернативы:

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. Когда линия действий будет готова, поток будет уничтожен самостоятельно. "Глобальный" объект события может использоваться для отмены действий вручную или во время выключения программы.

Ответ 3

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.

Ответ 4

После энергичного тестирования сна и сна... Я решил, что вариант "без сна" гораздо менее склонен дать пользователю ощущение, что в программном обеспечении есть ситуация блокировки.

uses System.Math, System.Types, System.Classes, System.Diagnostics, Vcl.Forms;

// written by Joseph A Poirier, 2016.06-17, converted 2016.12, added try-finally 2017.01-31 
// TThread.sleep(ms) replaced with good old-fashioned math.
// Pause a process, while allowing your forms to continue working,
// move, resize, and close (even mid-process).
procedure handleEvents(Milliseconds: Integer);
 var
   DoneTime, TimeLeft : LongWord;

   sw: TStopwatch;
begin

  DoneTime :=  TThread.GetTickCount + LongWord(Milliseconds);

  TimeLeft := 100;
  sw := TStopwatch.StartNew; // timing entire process
  try
    While ( TimeLeft > 0 ) do
     begin
       TimeLeft := DoneTime - TThread.GetTickCount;

       // continuously check application processes every 3 ms.
       if (TimeLeft mod 3 = 0) or ( TThread.GetTickCount > DoneTime ) then
        begin
         Application.ProcessMessages(); // clear all Vcl messages  // part of Vcl.Forms
         CheckSynchronize(); // check all threaded events
       end;

       if ( TThread.GetTickCount > DoneTime ) then break;
    end;
  finally // ensure stop is executed, whether or not close exceptions occur.
    sw.Stop;
    sw.ElapsedMilliseconds; // captures full run time for debug
  end;

end;

Ответ 5

Это процедура для ожидания определенного периода времени, пока он вызывает ProcessMessages (это для того, чтобы система реагировала).

procedure Delay(TickTime : Integer);
 var
 Past: longint;
 begin
 Past := GetTickCount;
 repeat
 application.ProcessMessages;
 Until (GetTickCount - Past) >= longint(TickTime);
end;