Une application Delphi sur laquelle je travaille doit attendre une ou parfois deux secondes. Je souhaite programmer ce délai en utilisant les meilleures pratiques. En lisant des entrées sur la méthode Sleep () de Delphi sur stackoverflow, j'ai trouvé ces deux commentaires:
Je vis selon cette maxime: "Si vous ressentez le besoin d'utiliser Sleep (), vous vous trompez." - Nick Hodges 12 mars 12 à 1:36
@ Nick en effet. Mon équivalent est "Il n'y a aucun problème pour lequel Sleep est la solution." - David Heffernan 12 mars 12 à 8:04
En réponse à ces conseils pour éviter d'appeler Sleep (), ainsi que ma compréhension de l'utilisation des classes TTimer et TEvent de Delphi, j'ai programmé le prototype suivant. Mes questions sont:
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;
Prendre vos questions tour à tour:
Oui (mais aussi "non" - voir ci-dessous).
La "bonne manière" varie selon les exigences spécifiques et le problème résolu. Il n'y a pas niversal Truth à ce sujet et quiconque vous dit le contraire essaie de vous vendre quelque chose (pour paraphraser).
Dans certains cas, attendre un événement est le mécanisme de retard approprié. Dans d'autres cas, non.
Voir ci-dessus: La réponse est oui. Cependant, cette deuxième question n'a tout simplement pas de sens car elle suppose que Sleep () est toujours et par nécessité jamais le bon qui, comme expliqué dans la réponse au point 1 ci-dessus, n'est pas nécessairement le cas.
Sleep () n'est peut-être pas le meilleur ou le plus approprié pour programmer un retard dans tous les scénarios, mais il existe des scénarios où il est le plus pratique et n'a pas d'inconvénients importants.
Pourquoi les gens évitent de dormir () ing
Sleep () est un problème potentiel précisément parce que c'est un retard inconditionnel qui ne peut pas être interrompu jusqu'à ce qu'une période de temps spécifique se soit écoulée. Les mécanismes de retard alternatifs réalisent généralement exactement la même chose, la seule différence étant qu'il existe un mécanisme alternatif pour reprendre l'exécution, autre que le simple passage du temps.
L'attente d'un événement retarde jusqu'à ce que l'événement se produise (ou soit détruit) o une période de temps spécifique s'est écoulée.
L'attente d'un mutex entraîne un retard jusqu'à ce que le mutex soit acquis (ou détruit) o une période de temps spécifique s'est écoulée.
etc.
En d'autres termes: alors que certains mécanismes de retard sont interruptibles. Sleep () ne l'est pas. Mais si vous vous trompez sur les autres mécanismes, il y a toujours la possibilité d'introduire des problèmes importants et souvent d'une manière qui peut être beaucoup plus difficile à identifier.
Problèmes avec Event.WaitFor () dans ce cas
Le prototype de la question met en évidence un problème potentiel d'utilisation de tout mécanisme qui suspend l'exécution de votre code si le reste de ce code n'est pas implémenté d'une manière qui est compatible avec cette approche particulière :
Form1.Timer1.Enabled := true;
Form1.EventManager.ResetEvent;
Form1.EventManager.WaitFor(INFINITE);
Si ce code est exécuté dans le thread principal, alors Timer1 ne se produira jamais.
Le prototype dans la question exécute cela dans un thread, donc ce problème particulier ne se pose pas, mais cela vaut la peine d'explorer le potentiel car le prototype introduit un problème différent en raison de l'implication de ce thread.
En spécifiant un INFINI délai d'attente sur votre WaitFor () sur l'événement, vous suspendez l'exécution du thread jusqu'à ce que cet événement se produise. Le composant TTimer utilise le mécanisme de temporisation basé sur les messages de Windows, dans lequel un message WM_TIMER est fourni à votre file d'attente de messages lorsque le temporisateur s'est écoulé. Pour que le message WM_TIMER se produise, votre application doit traiter sa file d'attente de messages.
Des minuteries Windows peuvent également être créées qui fourniront un rappel sur un autre thread, ce qui pourrait être une approche plus appropriée dans ce cas (certes artificiel). Cependant, ce n'est pas une capacité offerte par le composant VCL TTimer (à partir de XE4 au moins, et je note que vous utilisez XE2).
Problème # 1
Comme indiqué ci-dessus, les messages WM_TIMER dépendent du traitement par votre application de sa file d'attente de messages. Vous avez spécifié un minuteur de 2 secondes, mais si votre processus de candidature est occupé à effectuer d'autres travaux, le traitement de ce message peut potentiellement prendre plus de 2 secondes.
Il convient de mentionner ici que Sleep () est également soumis à une certaine inexactitude - il garantit qu'un thread est suspendu pour au moins la période spécifiée, il ne garantit pas exactement le délai spécifié.
Problème # 2
Le prototype met au point un mécanisme pour retarder de 2 secondes à l'aide d'une minuterie et d'un événement pour obtenir presque exactement le même résultat qui aurait pu être obtenu avec un simple appel à Sleep ().
La seule différence entre cela et un simple appel Sleep () est que votre thread reprendra également si l'événement qu'il attend est détruit.
Cependant, dans une situation réelle où certains traitements ultérieurs suivent le délai, il s'agit en soi d'un problème potentiellement important s'il n'est pas correctement géré. Dans le prototype, cette éventualité n'est pas du tout prise en compte. Même dans ce cas simple, il est très probable que si l'événement a été détruit, il en va de même pour le Timer1 que le thread tente de désactiver. Un Violation d'accès est susceptible de se produire dans le thread en conséquence lorsqu'il tente de désactiver ce temporisateur.
Développeur Caveat
Éviter dogmatiquement l'utilisation de Sleep () ne remplace pas une bonne compréhension de tous les mécanismes de synchronisation des threads (dont les retards ne sont qu'un) et la manière dont le système d'exploitation lui-même fonctionne, afin que la bonne technique peut être déployé selon les besoins.
En fait, dans le cas de votre prototype, Sleep () fournit sans doute la "meilleure" solution (si la fiabilité est la métrique clé) car la simplicité de cette technique garantit que votre code reprendra après 2 secondes sans tomber dans les pièges qui attendent les imprudents avec des techniques trop compliquées (par rapport au problème en question).
Cela dit, ce prototype est clairement un exemple artificiel.
D'après mon expérience, il y a très quelques situations pratiques où Sleep () est la solution optimale, bien que ce soit souvent le moins simple sujette aux erreurs. Mais je ne dirais jamais jamais.
Scénario: vous souhaitez effectuer des actions consécutives avec un certain délai entre elles.
Est-ce une bonne façon de programmer un retard?
Je dirais qu'il existe de meilleures façons, voir ci-dessous.
Si la réponse est oui, pourquoi est-ce mieux qu'un appel à Sleep ()?
Dormir dans le thread principal est une mauvaise idée: rappelez-vous, le paradigme Windows est piloté par les événements, c'est-à-dire que vous effectuez votre tâche en fonction d'une action, puis laissez le système gérer ce qui se passe ensuite. Dormir dans un thread est également mauvais, car vous pouvez bloquer les messages importants du système (en cas d'arrêt, etc.).
Vos options sont:
Gérez vos actions à partir d'une minuterie dans le thread principal comme une machine d'état. Gardez une trace de l'état et exécutez simplement l'action qui représente cet état particulier lorsque l'événement timer se déclenche. Cela fonctionne pour le code qui se termine en peu de temps pour chaque événement du minuteur.
Mettez la ligne d'actions dans un fil. Utilisez une temporisation d'événement comme minuterie pour éviter de geler le fil avec les appels en veille. Ces types d'actions sont souvent liés aux E/S, où vous appelez des fonctions avec un délai d'expiration intégré. Dans ces cas, le numéro de temporisation sert de retard naturel. C'est ainsi que toutes mes bibliothèques de communication sont construites.
Exemple de cette dernière alternative:
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;
Appeler:
var
SE: TSimpleEvent;
...
SE := TSimpleEvent.Create(Nil,False,False,'');
StartActions(SE);
Et pour abandonner les actions (en cas d'arrêt du programme ou d'abandon manuel):
SE.SetEvent;
...
FreeAndNil(SE);
Cela va créer un thread anonyme, où le timing est piloté par un TSimpleEvent
. Lorsque la ligne d'actions est prête, le fil sera auto-détruit. L'objet d'événement "global" peut être utilisé pour abandonner les actions manuellement ou pendant l'arrêt du programme.
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.