Delphi中repeat until循环中使用Sleep()函数出现奇怪的行为

5

我有一个函数,它会在按钮点击时执行。当我点击按钮时,它应该开始重复并在主窗体的标签中写入数组的值并显示它们。问题出在sleep函数上,可能是有什么bug,因为当我点击按钮时,它等待了相当长的时间,然后最终开始动作但非常快。让我们看看我的代码。感谢您的建议。

procedure TForm1.ButtonMereniClick(Sender: TObject);
var
  iterator: Integer;
begin      
  iterator := 1;
  repeat       
    //write some values stored int arrays to labels on form
    LabelTeplota.Caption:='Teplota: '+FloatToStr(poleTeplota[iterator]);
    LabelVlhkost.Caption:='Vlhkost: '+FloatToStr(poleVlhkost[iterator]);
    LabelTlak.Caption:='Atmosférický tlak: '+FloatToStr(poleTlak[iterator]);
    LabelRychlost.Caption:='Rychlost větru: '+FloatToStr(poleRychlost[iterator]);
    LabelRychlost.Caption:='Rychlost větru: '+FloatToStr(poleRychlost[iterator]);
    LabelIterator.Caption:='iterator: '+IntToStr(iterator);
    Sleep(500);//should be 5000 
    Inc(iterator);
  until iterator = 20;
end;

11
我奉行这样的信条:"如果你感觉需要使用Sleep()函数,那么你做错了什么。" - Nick Hodges
@nick 确实如此。我的等价说法是“没有问题是睡眠可以解决的。” - David Heffernan
6
@NickHodges 等。那么,当一个线程堆栈多层嵌套时,如何满足暂停要求呢?让我猜猜 - “花费很长时间重新实现过程化编写的规范为状态机,这样就可以使用计时器而不是 sleep()调用”。毫无疑问,在 GUI 处理程序中 A.P + sleep()循环是特别糟糕的滥用,但“sleep 可以被滥用,因此不要使用它” 的论点是...“难以支持”的。 - Martin James
1
@DavidHeffernan - 如果你所说的“不可中断”意味着在时间间隔结束之前,其他用户线程不能改变该线程的状态,那么是的。如果需要以这种方式通知线程,则不要使用sleep()。使用带有超时的事件或信号量等待即可满足此要求。如果没有这样的通知需求,则可以使用sleep()。 - Martin James
1
总之,像 Sleep(X); 这样的简单代码行,其中X是一个像60,000这样的巨大数字,那么应用程序将会在1分钟(60,000毫秒)内被冻结Uninterruptable 大致翻译为“无法告诉应用程序停止 Sleep 命令”。 - Jerry Dodge
显示剩余5条评论
4个回答

19
不要在GUI线程中使用Sleep函数,因为它会阻止正常的消息处理,并导致应用程序行为异常。
不清楚你为什么在代码中使用Sleep。可能应该从TTimer组件的OnTimer事件处理程序更新标签,而不是使用循环。

2
+1 这是正确的答案。阻塞 GUI 线程总是一个不好的做法。使用计时器。 - David Heffernan

1
我使用了两个定时器来“缓慢”地更新一些值(以便用户可以看到进展),而不会减慢应用程序或在最坏的情况下减慢系统。
//  "weights" for old and new value; I was working on integers
var
  Wa: integer =  1;
  Wb: integer =  9;
  Wt: integer = 10;

//  interval of 1000 ms, of course
procedure frmMain.T1000Timer(Sender: TObject);
begin
  Wa:= 1;
  nValue:= calculate_new_value;
end;

//  100 ms interval
procedure frmMain.T100Timer(Sender: TObject);
begin
  Wb:= Wt -Wa;
  //  displayed value, ie gauge.progress, or something.Value, etc.
  sam.Value:= Round( (Wb *sam.Value +Wa *nValue) /Wt );
  if Wa < Wt then Inc(Wa);
end;

0
如果您必须使用延迟或“睡眠”类型的函数,可以使用带有ProcessMessages过程的程序。使用它有一些优点和缺点,但我已经成功地在许多情况下使用它,没有任何不良影响。我知道这里还有其他人可以更好地评论ProcessMessages。
Procedure Delay(MSecs: Cardinal);
var
 FirstTick, CurrentTick : Cardinal;
 Done : Boolean;
begin
 Done := FALSE;
 FirstTick := GetTickCount;
 While Not Done do
  begin
   Application.ProcessMessages;
   CurrentTick := GetTickCount;
   If Int64(CurrentTick) - Int64(FirstTick) < 0 Then
    begin
     If CurrentTick >= (Int64(FirstTick) - High(Cardinal) + MSecs) Then
      Done := TRUE;
       End
        Else
         If CurrentTick - FirstTick >= MSecs Then
          Done := TRUE;
  end;
end;

// Below for a service

procedure YourSvrSvc.ProcessMessages;
var
  Msg: TMsg;
begin
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
  begin
    TranslateMessage(Msg);
    DispatchMessage(Msg);
  end;
end;

Procedure YOURSvrSvc.Delay(MSecs: Cardinal);
var
 FirstTick, CurrentTick : Cardinal;
 Done : Boolean;
begin
 Done := FALSE;
 FirstTick := GetTickCount;
 While Not Done do
  begin
   YOURSvrSvc.ProcessMessages;
   CurrentTick := GetTickCount;
   If Int64(CurrentTick) - Int64(FirstTick) < 0 Then
    begin
     If CurrentTick >= (Int64(FirstTick) - High(Cardinal) + MSecs) Then
      Done := TRUE;
       End
        Else
         If CurrentTick - FirstTick >= MSecs Then
          Done := TRUE;
  end;
end;

5
ProcessMessages 打开了你的事件处理程序,使其容易发生可重入调用。 - David Heffernan
@David 是的先生!我确实提到了有利和不利的方面,但我认为这可能超出了原帖的范围。但我给了他一个选择。 - John
2
我只看到使用ProcessMessages的缺点。 - David Heffernan
更不用提繁忙的循环了。 - David Heffernan
1
在这里,我完全同意@DavidHeffernan的观点。A.P是设计失败的承认。不必要的轮询、延迟和重入风险 :(( - Martin James

0

对于我来说,很难说不要使用Sleep,因为我自己经常使用它,但Application.ProcessMessages实际上是一个危险的解决方案,特别是在循环中使用时。我不确定您显示了什么信息(因为我不认识该语言),但看起来您正在执行一些从Float到String的转换。尽管这些转换在瞬间完成,但将它们全部加起来,您会得到一个漫长的操作。而且,假设您决定添加另一个需要一些计算的值进行更新(例如文件传输中的每秒字节数)。这种转换将为该操作增加更多时间,而在您意识到之前,您可能会得到需要占用相当大处理器资源的UI更新,甚至需要半秒钟的时间。

因此,我建议使用线程来执行所有这些转换、计算等操作,并在信息发生更改时根据需要触发事件。现在,相比其他建议的解决方案,使用线程肯定会更复杂,毫无疑问。但使用线程也可以带来许多好处。您所有的重活都可以在后台完成,而您的应用程序仍然可以完美地响应。请记住,在涉及UI更新时,使用线程可能非常棘手。

有几种方法可以创建线程,但我会尝试让它简单...

type
  TMyThread = class;

  TMyThreadEvent = procedure(Sender: TObject; const Val1, Val2: String) of object;

  TMyThread = class(TThread)
  private
    FValue1: Integer;
    FValue2: Integer;
    FString1: String;
    FString2: String;
    FOnChange: TMyThreadEvent;
    procedure SYNC_OnChange;
  protected
    procedure Execute; override;
  public
    constructor Create;
    property Value1: Integer read FValue1 write FValue1;
    property Value2: Integer read FValue2 write FValue1;
    property String1: String read FString1;
    property String2: String read FString2;
    property OnChange: TMyThreadEvent read FOnChange write FOnChange;
  end;

  ...

  constructor TMyThread.Create;
  begin
    inherited Create(False);
    FValue1 := '0';
    FValue2 := '0';
  end;

  procedure TMyThread.Execute;
  var
    S1, S2: String;
    DoChange: Bool;
  begin
    DoChange:= False;
    FValue2:= DoSomeBigCalculation(FValue1); //Some random big calculation
    S1:= FormatFloat('#,##0.#', FValue1);
    S2:= FormatFloat('#,##0.#', FValue2);
    if (S1 <> FString1) then begin
      FString1:= S1;
      DoChange:= True;
    end;
    if (S2 <> FString2) then begin
      FString2:= S2;
      DoChange:= True;
    end;
    if DoChange then
      Synchronize(SYNC_OnChange);
  end;

  procedure TMyThread.SYNC_OnChange;
  begin
    if assigned(FOnChange) then
      FOnChange(Self, FString1, FString2);
  end;

现在,要使用它,您需要根据需要设置 Integer 属性。确保将 OnChange 事件设置为具有上述 TMyThreadEvent 类型参数的过程。每当任何值与其原始(或旧)值不同时,都会触发此事件。我还强烈建议您将首先产生这些值的任何处理代码放在一个线程内。多线程的可能性是广泛的,并在具有许多功能的应用程序中证明了巨大的优势。

请注意,我上面的代码只是直接键入此网站的示例,未经测试。这只是为了让您了解如何实现线程来更新内容。


你可以更进一步,创建单独的事件,每个属性对应一个事件。这样就会有像 OnTeplotaOnVlhkost 等返回新值的事件。 - Jerry Dodge

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