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无需将整个文件加载到内存中的
TStringList本身就是慢课。它有很多 - 花里胡哨 - 额外的特性和功能,使其陷入困境。更快的容器将是TList<String>或普通的旧动态array of string。见System.IOUTils.TFile.ReadAllLines功能。
阅读有关堆内存碎片的信息,例如http://en.wikipedia.org/Heap_fragmentation
即使没有内存泄漏,它也可能发生并破坏您的应用程序。但既然你说有很多小泄漏——这就是最有可能发生的。您可以通过避免将整个文件读入内存并使用较小的块进行操作来或多或少地延迟崩溃。但是退化仍然会继续,甚至更慢,最后你的程序会再次崩溃。
附注。一般注意事项。
我认为您的团队应该重新考虑您对多线程的需求。坦率地说,我看不到。您正在从 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)
这是正确的方法:
所以,有时候,
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- 它应该做什么?它会做随机垃圾。
所以,重复以上所有内容。
try关键字之前分配(初始化)。如果您保护文件 - 然后在try. 如果您防止内存泄漏 - 之前创建对象try。等等。不要在try操作符之后进行我们的第一次初始化- WITHIN try-block - 现在为时已晚。try关键字上方。更好的是,在该分配之前插入一个空行。让你(或任何其他读者)意识到这个变量和这个尝试是相互依赖的,永远不应该分开。| 归档时间: |
|
| 查看次数: |
702 次 |
| 最近记录: |