使用 TStringList 的奇怪 EOutOfMemory 异常

Tio*_*des 0 delphi tstringlist text-files delphi-10.1-berlin

我有一个系统,它加载一些压缩到“.log”文件中的文本文件,然后使用多个线程将其解析为信息类,每个线程处理不同的文件并将解析的对象添加到列表中。该文件是使用 TStringList 加载的,因为它是我测试过的最快的方法。

文本文件的数量是可变的,但通常我必须在一次入侵中处理 5 到 8 个文件,范围从 50Mb 到 120Mb。

我的问题:用户可以根据需要多次加载 .log 文件,在其中一些进程之后,我在尝试使用 TStringList.LoadFromFile 时收到 EOutOfMemory 异常。当然,任何使用过 StringList 的人首先想到的是在处理大文本文件时不应该使用它,但是这个异常是随机发生的,并且在该过程至少成功完成一次之后(对象在新解析开始之前被销毁,因此除了一些小泄漏之外,内存可以正确检索)

我尝试使用textile 和TStreamReader,但它不如TStringList 快,而且这个过程的持续时间是这个功能最关心的问题。

我正在使用 10.1 Berlin,解析过程是一个简单的迭代,通过不同长度的线列表和基于线信息的对象构造。

本质上,我的问题是,是什么导致了这种情况,我该如何解决。我可以使用其他方式加载文件并读取其内容,但它必须与 TStringList 方法一样快(或更好)。

加载线程执行代码:

TThreadFactory= class(TThread)
  protected
     // Class that holds the list of Commands already parsed, is owned outside of the thread
    _logFile: TLogFile;
    _criticalSection: TCriticalSection;
    _error: string;

    procedure Execute; override;
    destructor Destroy; override;

  public
    constructor Create(AFile: TLogFile; ASection: TCriticalSection); overload;

    property Error: string read _error;

  end;

implementation


{ TThreadFactory}

    constructor TThreadFactory.Create(AFile: TLogFile; ASection: TCriticalSection);
    begin
      inherited Create(True);
      _logFile := AFile;

      _criticalSection := ASection;
    end;


    procedure TThreadFactory.Execute;
        var
          tmpLogFile: TStringList;
          tmpConvertedList: TList<TLogCommand>;
          tmpCommand: TLogCommand;
          tmpLine: string;
          i: Integer;
        begin
          try
            try
              tmpConvertedList:= TList<TLogCommand>.Create;       

                if (_path <> '') and not(Terminated) then
                begin

                  try
                    logFile:= TStringList.Create;
                    logFile.LoadFromFile(tmpCaminho);

                    for tmpLine in logFile do
                    begin
                      if Terminated then
                        Break;

                      if (tmpLine <> '') then
                      begin
                        // the logic here was simplified that's just that 
                        tmpConvertedList.Add(TLogCommand.Create(tmpLine)); 
                      end;
                    end;
                  finally
                    logFile.Free;
                  end;

                end;


              _cricticalSection.Acquire;

              _logFile.AddCommands(tmpConvertedList);
            finally
              _cricticalSection.Release;

              FreeAndNil(tmpConvertedList);    
            end;
          Except
            on e: Exception do
              _error := e.Message;
          end;
        end;

    end.     
Run Code Online (Sandbox Code Playgroud)

补充:感谢您的所有反馈。我将解决一些讨论过但我在最初的问题中没有提到的问题。

  • .log 文件里面有多个 .txt 文件的实例,但它也可以有多个 .log 文件,每个文件代表一天的日志记录或用户选择的时间段,因为解压需要很多时间启动一个线程每次找到 .txt 时,我就可以立即开始解析,这缩短了用户明显的等待时间

  • ReportMemoryLeaksOnShutdown 和其他方法(如 TStreamReader)未显示“轻微泄漏”,避免了此问题

  • 命令列表由 TLogFile 保存。这个类在任何时候都只有一个实例,并且在用户想要加载 .log 文件时被销毁。所有线程都向同一个对象添加命令,这就是临界区的原因。

  • 无法详细说明解析过程,因为它会透露一些合理的信息,但这是从字符串和 TCommand 中收集的简单信息

  • 从一开始我就知道碎片化,但我从未找到具体证据表明 TStringList 仅通过多次加载导致碎片化,如果可以确认这一点我会很高兴

谢谢您的关注。我最终使用了一个外部库,它能够以与以下相同的速度读取行和加载文件TStringList无需将整个文件加载到内存中的

https://github.com/d-mozulyov/CachedTexts/tree/master/lib

Ari*_*The 5

  1. TStringList本身就是慢课。它有很多 - 花里胡哨 - 额外的特性和功能,使其陷入困境。更快的容器将是TList<String>或普通的旧动态array of string。见System.IOUTils.TFile.ReadAllLines功能。

  2. 阅读有关堆内存碎片的信息,例如http://en.wikipedia.org/Heap_fragmentation

即使没有内存泄漏,它也可能发生并破坏您的应用程序。但既然你说有很多小泄漏——这就是最有可能发生的。您可以通过避免将整个文件读入内存并使用较小的块进行操作来或多或少地延迟崩溃。但是退化仍然会继续,甚至更慢,最后你的程序会再次崩溃。

  1. 有很多特别的类库,通过缓冲、预取等方式逐块读取大文件。其中一种针对文本的库是http://github.com/d-mozulyov/CachedTexts,还有其他库。

附注。一般注意事项。

我认为您的团队应该重新考虑您对多线程的需求。坦率地说,我看不到。您正在从 HDD 加载文件,并且可能将处理和转换的文件写入同一个(最好是另一个)HDD。这意味着,您的程序速度受磁盘速度的限制。而且这个速度远低于 CPU 和 RAM 的速度。通过引入多线程,您似乎只会使您的程序更加复杂和脆弱。错误更难检测,众所周知的库可能会突然在 MT 模式下行为异常等。而且您可能不会获得性能提升,因为瓶颈在于磁盘 I/O 速度。

如果您仍然需要多线程,那么也许可以查看 OmniThreading Library。它旨在简化开发“数据流”类型的 MT 应用程序。阅读教程和示例。

我绝对建议您消除所有那些“一些小漏洞”,并将其作为修复所有编译警告的一部分。我知道,当你不是项目中唯一的程序员并且其他人不在乎时,这很难。仍然“轻微泄漏”意味着您的团队中没有人知道程序的实际行为或行为。多线程环境中的非确定性随机行为很容易产生大量随机的 Shroeden 错误,您永远无法重现和修复这些错误。

你的try-finally模式真的被打破了。您在finally块中清理的变量应该在块之前分配try,而不是在块内!

o := TObject.Create;
try
  ....
finally
  o.Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

这是正确的方法:

  • 要么创建对象失败——那么 try-block 不会被输入,也不会被 finally-block。
  • 或者该对象已成功创建 - 然后将进入 try-block,因此将进入 finally-block

所以,有时候,

o := nil;
try
  o := TObject.Create;
  ....
finally
  o.Free;
end;
Run Code Online (Sandbox Code Playgroud)

这也是正确的。该变量设置为nil 紧接在进入 try-block之前。如果对象创建失败,那么当 finally-blocks 调用Free方法时,变量已经被赋值,并且TObject.Free(但不是TObject.Destroy)被设计为能够处理nil对象引用。其本身只是第一个的嘈杂,过于冗长的修改,但它可以作为更多衍生产品的基础。

当您不知道是否会创建对象时,可以使用该模式。

o := nil;
try
  ...
  if SomeConditionCheck() 
     then o := TObject.Create;  // but maybe not
  ....
finally
  o.Free;
end;
Run Code Online (Sandbox Code Playgroud)

或者当对象创建被延迟时,因为你需要为它的创建计算一些数据,或者因为对象很重(例如全局阻塞对某些文件的访问)所以你努力保持它的生命周期尽可能短。

o := nil;
try
  ...some code that may raise errors
  o := TObject.Create; 
  ....
finally
  o.Free;
end;
Run Code Online (Sandbox Code Playgroud)

该代码虽然询问为什么所说的“...一些代码”没有移到try块之外和之前。通常它可以而且应该是。比较少见的图案。

创建多个对象时,会使用该模式的另一种派生形式;

o1 := nil;
o2 := nil;
o3 := nil;
try
  o2 := TObject.Create;
  o3 := TObject.Create;
  o1 := TObject.Create;
  ....
finally
  o3.Free;
  o2.Free;
  o1.Free;
end;
Run Code Online (Sandbox Code Playgroud)

目标是,例如,如果o3对象创建失败,o1则将被释放并且o2未被创建,并且Freefinally-block 中的调用会知道它。

这是半正确的。假定销毁对象永远不会引发自己的异常。通常这种假设是正确的,但并非总是如此。无论如何,这种模式可以让你将几个 try-finally 块融合为一个,这使得源代码更短(更容易阅读和推理)并执行得更快一点。通常这也是相当安全的,但并非总是如此。

现在有两种典型的模式误用:

o := TObject.Create;
..... some extra code here
try
  ....
finally
  o.Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

如果对象创建和 try-block 之间的代码引发了一些错误 - 那么没有人可以释放该对象。你刚刚有内存泄漏。

当您阅读 Delphi 源代码时,您可能会看到类似的模式

with TObject.Create do
try
  ....some very short code
finally
  Destroy;
end;
Run Code Online (Sandbox Code Playgroud)

由于对with构造的任何使用有着广泛的热情,这种模式排除了在对象创建和尝试保护之间添加额外代码的可能性。典型的with缺点 - 可能的命名空间冲突和无法将此匿名对象作为参数传递给其他函数 - 包括在内。

另一个不幸的修改:

o := nil;
..... some extra code here
..... that does never change o value
..... and our fortuneteller warrants never it would become
..... we know it for sure
try
  ....
  o := TObject.Create;
  ....
finally
  o.Free;
end;
Run Code Online (Sandbox Code Playgroud)

这种模式在技术上是正确的,但在这方面相当脆弱。您不会立即看到o := nilline 和 try-block之间的链接。当您将来开发程序时,您可能很容易忘记它并引入错误:例如复制粘贴/将 try-block 移动到另一个函数中并忘记 nil-initializing。或者扩展中间代码并使其使用(从而更改) that 的值o。有一种情况我有时会使用它,但它非常罕见并且带有风险。

现在,

...some random code here that does not
...initialize o variable, so the o contains
...random memory garbage here
try
  o := TObject.Create;
  ....
finally
  o.Destroy; // or o.Free
end;
Run Code Online (Sandbox Code Playgroud)

这是你写了很多没有考虑 try-finally 是如何工作的以及它为什么被发明的原因。问题很简单:当您进入 try-block 时,您的o变量是一个带有随机垃圾的容器。现在,当您尝试创建对象时,您可能会遇到一些引发的错误。然后怎样呢?然后你进入 finally-block 并调用(random-garbage).Free- 它应该做什么?它会做随机垃圾。

所以,重复以上所有内容。

  1. try-finally 用于保证对象释放或任何其他变量清理(关闭文件、关闭窗口等),因此:
  2. 用于跟踪该资源(例如对象引用)的变量在进入 try 块时应该具有众所周知的值,它应该在try关键字之前分配(初始化)。如果您保护文件 - 然后在try. 如果您防止内存泄漏 - 之前创建对象try。等等。不要在try操作符之后进行我们的第一次初始化- WITHIN try-block - 现在为时已晚。
  3. 您最好将代码设计得尽可能简单(不言自明),从而消除在您忘记今天留在脑海中的非明确隐藏假设时引入未来错误的可能性 - 并且会交叉它们。看看谁写了这个程序说?“总是编码,好像最终维护你的代码的人将是一个知道你住在哪里的暴力精神病患者。”. 这里的意思是,在块开始之前立即初始化(分配)由 try-block 保护的变量,就在try关键字上方。更好的是,在该分配之前插入一个空行。让你(或任何其他读者)意识到这个变量和这个尝试是相互依赖的,永远不应该分开。

  • 很好的答案,感谢您抽出时间关注我们的应用程序,正如我所说,泄漏已经消失了,因为它们最初是由于对“TStringList”的一些滥用造成的。另外,感谢关​​于“try-finally”的课程 (2认同)