使用 TStringList 的奇怪 EOutOfMemory 异常

Strange EOutOfMemory exception using TStringList

我有一个系统可以加载一些压缩到“.log”文件中的文本文件,然后使用多个线程将其解析为信息性 classes,每个线程处理不同的文件并添加已解析的对象到一个列表。 该文件是使用 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.     

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

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

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

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

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

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

  1. 有很多临时 classes 库,通过缓冲、预取等等读取大文件。 http://github.com/d-mozulyov/CachedTexts 就是其中一种以文本为目标的图书馆,还有其他图书馆。

PS。一般注意事项。

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

如果您仍然想要多线程,那么或许可以查看 OmniThreading Library。它旨在简化开发 "data streams" 类型的 MT 应用程序。阅读教程和示例。

我绝对建议您压缩所有这些 "some minor leaks" 并作为其中的一部分来修复所有编译警告。我知道,当您不是项目中唯一的程序员而其他人不在乎时,这很难。 "minor leaks" 仍然意味着您的团队中的 none 知道程序的实际行为或行为方式。多线程环境中的非确定性随机行为很容易产生大量随机的 Shroeden 错误,您将永远无法重现和修复这些错误。

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

o := TObject.Create;
try
  ....
finally
  o.Destroy;
end;

这是正确的方法:

  • 要么对象创建失败 - 则不会进入 try-block,也不会进入 finally-block。
  • 或者对象创建成功 - 然后进入try-block,最后进入finally-block

所以,有时候,

o := nil;
try
  o := TObject.Create;
  ....
finally
  o.Free;
end;

这也是对的。在输入 try-block 之前,变量立即设置为 nil 。如果对象创建失败,那么当 finally-blocks 调用 Free 方法时,变量已经被分配,并且 TObject.Free(但不是 TObject.Destroy)被设计为能够在 [=26= 上工作] 对象引用。它本身只是对第一个的嘈杂、过于冗长的修改,但它是更多衍生产品的基础。

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

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

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

o := nil;
try
  ...some code that may raise errors
  o := TObject.Create; 
  ....
finally
  o.Free;
end;

虽然该代码询问为什么没有将所述“...一些代码”移到 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;

目标是,如果例如 o3 对象创建失败,那么 o1 将被释放并且 o2 不会被创建并且 Free 在 finally-block 中调用会知道的。

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

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

o := TObject.Create;
..... some extra code here
try
  ....
finally
  o.Destroy;
end;

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

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

with TObject.Create do
try
  ....some very short code
finally
  Destroy;
end;

鉴于反对使用任何 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;

这个模式在技术上是正确的,但在这方面相当脆弱。 您不会立即看到 o := nil 行和 try 块之间的 link。 以后开发程序的时候,很容易就忘记了,引入错误:比如copy-pasting/moving try-block 到另一个函数中,忘记了nil 初始化。或者扩展中间代码并使其使用(因此 - 更改)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;

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

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

  1. try-finally 用于保证对象释放或任何其他变量清理(关闭文件、关闭 windows 等),因此:
  2. 用于跟踪该资源(例如对象引用)的变量应该在 try 块的入口处具有众所周知的值,它应该在 try 关键字之前分配(初始化)。如果您保护文件 - 然后在 try 之前立即打开它。如果您防止内存泄漏 - 在 try 之前创建对象。等等。不要在 try 运算符之后进行我们的第一次初始化 - 在 try-block 中 - 那里为时已晚。
  3. 您最好将代码设计得尽可能简单(不言而喻),从而消除在您忘记今天留在脑海角落的非显式隐藏假设时引入未来错误的可能性 - 并且会越过它们.参见 Who wrote this programing saying? "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."。这里的意思是,在块开始之前立即初始化(分配)由 try-block 保护的变量,就在 try 关键字上方。更好的是,在该分配之前插入一个空行。让你(或任何其他 reader)明白这个变量和这个 try 是相互依赖的,永远不应该分开。