为什么Delphi进度条会增加迭代过程的执行时间?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么Delphi进度条会增加迭代过程的执行时间?相关的知识,希望对你有一定的参考价值。

为什么使用进度条显示迭代的进度会大大增加相关进程的执行时间?考虑以下示例:

procedure FileToStringList(FileName: String);
var
  fileSource: TStringList;
  I: Integer;
begin
  fileSource:= TStringList.Create;
  try
    fileSource.LoadFromFile(FileName);
    for I := 0 to fileSource.Count - 1 do
      begin
       //Code....
      end;
  finally
    fileSource.Free;
  end;
end;

如果添加进度条的更新:

procedure FileToStringList(FileName: String);
var
  fileSource: TStringList;
  I: Integer;
begin
  fileSource:= TStringList.Create;
  try
    fileSource.LoadFromFile(FileName);
    ProgressBar.Properties.Max:= fileSource.Count;
    for I := 0 to fileSource.Count - 1 do
      begin
        Application.ProcessMessages;
        ProgressBar.Position:= I;
      end;
  finally
    fileSource.Free;
  end;
end;

迭代过程所需的时间大大增加。

在没有更新进度条的情况下对200,000行的文件执行读取测试,迭代的时间大约为8秒,但是,如果激活进度条的更新以显示迭代的进度,则此过程需要几分钟。

文件为2,700行的测试,正常时间为2-4秒,但使用进度条,执行时间超过1分钟。

有人可以指出Application ProcessMessages的使用是否不正确?如果例程在单元中或与进度条相同的表单上,则结果不会更改。

好的,我可以看到评论,但有人可以通过示例或链接指出在这些条件下更新进度条的正确方法吗?

答案

如果您的应用程序仅转换输入文件,则不需要特殊线程,您可以使用以下代码。

在应用程序允许用户在处理文件的同时执行另一操作的情况下,应该使用后台线程。

procedure FileToStringList(FileName: String);
var
   fileSource: TStringList;
   I,J: Integer;
begin
  fileSource:= TStringList.Create;
  try
    fileSource.LoadFromFile(FileName);
    ProgressBar.Properties.Max:= fileSource.Count;
    J:=10;//TODO make it better
    for I := 0 to fileSource.Count - 1 do
    begin
      if (I mod J = 0) then
      begin
        Application.ProcessMessages;
        ProgressBar.Position:= I;
      end;
    end;
    ProgressBar.Position:= fileSource.Count;
  finally
    fileSource.Free;
  end;
end;

或者您可以按时调用您的processMessages:

procedure FileToStringList(FileName: String);
var
   fileSource: TStringList;
   I,J: Integer;
   lastCheck: TDateTime;
begin
  fileSource:= TStringList.Create;
  try
    fileSource.LoadFromFile(FileName);
    ProgressBar.Properties.Max:= fileSource.Count;
    J:=1000;//refresh in ms
    lastCheck:=now;
    for I := 0 to fileSource.Count - 1 do
    begin
      if (lastCheck+j)<now then
      begin
        lastCheck:=now;
        Application.ProcessMessages;
        ProgressBar.Position:= I;
      end;
    end;
    ProgressBar.Position:= fileSource.Count;
  finally
    fileSource.Free;
  end;
end;
另一答案

你真的不是“只是添加一个进度条”。您对Application.ProcessMessages;消息的使用意味着您还在进行其他工作的各种其他消息。所以现在你的“忙/主工作”在同一个线程(和CPU)上竞争CPU时间与通过你的应用程序的所有其他消息竞争。我们当然无法评论您的应用程序可能会传递哪些其他消息。

繁忙的工作不应该在主线程中完成。并且将方法包装到线程中通常相当容易,前提是它尚未与GUI紧密耦合。

毫无疑问,你已经在评论中告诉了所有这些。


首先要注意一些重要的规则:

  • 不要从子线程与GUI交互(同步或队列调用更新GUI的代码);
  • 避免在线程之间共享data1(包括主线程);
  • 如果你必须共享数据,请确保你的线程协调他们的访问权限,以避免竞争条件(太详细的主题)。

然后以下是最低要求:

  1. 定义你的线程。
  2. 使用Execute()方法实现主要处理。
  3. 创建并启动您的线程。
  4. 由于您要更新进度条并记住“要记录的规则”:请确保对这些更新进行排队。

但是,您可以应用许多更高级的注意事项来改进您的线程。 (这些将留待您进一步研究。)

  1. 您也建议您也接受Ive和其他人已经提出的建议并减少更新进度的次数。过多的更新只会浪费时间;特别是跨线程操作(参见上一节)。
  2. 如果您的用户想要取消作业或关闭应用程序,您如何打断您的主题?
  3. 您如何管理用户开始工作太多的可能性?
  4. 你如何处理线程中的错误?
  5. 您希望如何管理线程终止时发生的事情。

以下示例代码是1-4中所需内容的精简版本。你可以填写我遗漏的琐碎位。

1)

type
  TFileProcessor = class(TThread)
  public
    constructor Create(const AFileName: string);
    procedure Execute; override;
  end;

2 & 4)

请注意,您可以在构造函数中传递进度条实例并从线程更新它。但即使它有点多工作,在你的线程上定义一个回调事件也会更加清晰,并允许你的GUI处理事件,以便准确选择它想做的事情。

procedure TFileProcessor.Execute;
var
  fileSource: TStringList;
  I: Integer;
begin
  fileSource:= TStringList.Create;
  try
    fileSource.LoadFromFile(FileName);
    { GUI interaction must be queued.
      ProgressBar.Properties.Max:= fileSource.Count;}
      FPosition := 0;
      FCount := fileSource.Count;
      Queue(DoUpdateProgress);
    for I := 0 to fileSource.Count - 1 do
      begin
        { Obviously this must go!
          Application.ProcessMessages;}
        { Again GUI interaction must be Queued
          ProgressBar.Position:= I;}
        FPosition := I;
        Queue(DoUpdateProgress); {TIP: Reduce your progress updates for 
                                  more performance improvement; updating 
                                  on every single line is overkill.}
      end;
  finally
    fileSource.Free;
  end;
end;

procedure TFileProcessor.DoUpdateProgress();
begin
  if Assigned(FOnUpdateProgress) then
    FOnUpdateProgress(FPosition, FCount);
end;

3)

procedure TForm1.Button1Click(...);
var
  LThread: TFileProcessor;
begin
  LThread := TFileProcessor.Create(FFileName);
  LThread.OnUpdateProgress := HandleUpdateProgress;
  LThread.FreeOnTerminate := True;
  LThread.Start;
end;

4)

如前所述,如果您的表单控制它想要更新的GUI控件以及响应进度更新的方式,那么它会更清晰。例如。如果需要,您可以在同一时间更新标签,而无需更改线程和作业代码。

procedure TForm1.HandleUpdateProgress(APosition, ACount: Integer);
begin
  ProgressBar.Position := APosition;
  ProgressBar.Properties.Max := ACount;
  Label1.Caption := Format('Line %d of %d', [APosition, ACount]);
end;

1我想强调你应该避免使用多线程代码共享数据。跨线程操作比同线程操作昂贵得多。 (这包括通知主线程。)

例如,在我的系统上,上面的线程代码具有以下开销。

  • 主线程的200,000个排队事件的开销几乎为1秒。
  • 根据您在HandleUpdateProgress中的操作,您可能会发现在文件实际完成处理后,需要一些时间处理所有排队的消息。 (在我的系统上更新标准标签和进度条,这需要5秒钟。)

以上是关于为什么Delphi进度条会增加迭代过程的执行时间?的主要内容,如果未能解决你的问题,请参考以下文章

Python 进度条

Python进度条

Flutter 进度条

Flutter 进度条

Delphi x64:如何将本地过程作为过程参数(回调)传递

动画视图仅缩放屏幕宽度的一半