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

时间:2018-03-02 11:20:43

标签: 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的使用是否不正确?如果例程在单元中或与进度条相同的表单上,结果不会改变。

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

2 个答案:

答案 0 :(得分:0)

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

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

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

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

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

然后以下是最低要求:

  1. 定义你的主题。
  2. 使用Execute()方法实施主要处理。
  3. 创建并启动您的主题。
  4. 由于您要更新进度条,并记住"要记录的规则":请确保对这些更新进行排队。
  5. 但是,您可以应用许多更高级的注意事项来改进您的线程。 (这些将留待您进一步研究。)

    1. 建议你也建议Ive和其他人已经给出建议并减少你更新进度的次数。过多的更新只会浪费时间;特别是跨线程操作(参见上一节)。
    2. 如果您的用户想要取消作业或关闭应用,您如何打断线程?
    3. 您如何管理用户开始工作太多的可能性?
    4. 你如何处理线程中的错误?
    5. 您希望如何管理线程终止时发生的事情。
    6. 以下示例代码是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秒钟。)

答案 1 :(得分:-1)

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

如果应用程序允许用户在处理文件时执行其他操作,则应使用后台线程。

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;