我有一个系统可以加载一些压缩成“.log”文件的文本文件,然后使用多个线程解析为信息类,每个线程处理不同的文件并将解析后的对象添加到列表中。 该文件使用TStringList加载,因为它是我测试过的最快的方法。
文本文件的数量是可变的,但通常我必须在一次入侵中处理5到8个文件,范围从50Mb到120Mb。
我的问题:用户可以根据需要多次加载.log文件,并且在尝试使用TStringList.LoadFromFile之后,我会在其中一些进程中收到EOutOfMemory异常。当然,任何曾经使用过StringList的人都会想到的第一件事是你在处理大文本文件时不应该使用它,但是这个异常是随机发生的,并且在该过程已经成功完成至少一次之后(在新的解析开始之前销毁对象,以便除了一些小的泄漏之外正确检索内存)
我尝试使用textile和TStreamReader,但它没有TStringList那么快,并且该过程的持续时间是此功能最关注的问题。
我正在使用10.1柏林,解析过程是一个简单的迭代,通过不同长度的行列表和基于行信息的对象构造。
基本上,我的问题是,导致这种情况的原因以及如何解决这个问题。我可以使用其他方式加载文件并读取其内容,但它必须与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.
补充:感谢您的所有反馈。我将讨论一些已经讨论过的问题,但我在最初的问题中没有提到。
.log文件里面有多个.txt文件实例,但它也可以有多个.log文件,每个文件代表一天的日志记录或用户选择的一段时间,因为解压缩需要很多每次找到.txt时启动一个线程的时间,这样我就可以立即开始解析,这缩短了用户明显的等待时间
ReportMemoryLeaksOnShutdown不会显示“次要泄漏”,而TStreamReader等其他方法也不会出现此问题
命令列表由TLogFile保存。这个类在任何时候只有一个实例,并且只要用户想要加载.log文件就会被销毁。 所有线程都将命令添加到同一个对象,这就是关键部分的原因。
无法详细说明解析过程,因为它会披露一些明智的信息,但这是从字符串和TCommand收集的简单信息
从一开始我就意识到了碎片,但我从来没有找到具体的证据证明TStringList只是通过多次加载导致碎片,如果可以确认我会很高兴
感谢您的关注。我最终使用了一个能够以TStringList
的速度读取行和加载文件的外部库,而无需将整个文件加载到内存中
答案 0 :(得分:4)
TStringList
本身很慢。它有很多铃声和口哨声 - 额外的功能和功能让它陷入困境。更快的容器将是TList<String>
或普通的动态array of string
。请参阅System.IOUTils.TFile.ReadAllLines
功能。
即使没有内存泄漏,它也可能发生并破坏您的应用程序。 但既然你说有很多小泄漏 - 那就是最有可能发生的事情。您可以通过避免将整个文件读入内存并使用较小的块进行操作来或多或少地延迟崩溃。但是降级仍会继续,甚至更慢,最终你的程序会再次崩溃。
PS。一般说明。
我认为您的团队应该重新考虑您对多线程的需求。 坦率地说,我没有看到。 您正在从HDD加载文件,并且可能将处理和转换的文件写入相同的(最好是另一个)HDD。 这意味着,您的程序速度受磁盘速度的限制。而且速度远远低于CPU和RAM的速度。 通过引入多线程,您似乎只会使您的程序更加复杂和脆弱。错误很难被发现,众所周知的库可能会突然在MT模式下行为不端等等。而且你可能没有性能提升,因为瓶颈是磁盘I / O速度。
如果您仍然想要多线程 - 那么也许可以查看OmniThreading库。它旨在简化开发和#34;数据流&#34; MT应用程序的类型。阅读教程和示例。
我绝对建议你压扁所有这些&#34;一些小漏洞&#34;并作为修复所有编译警告的一部分。我知道,当你不是该项目的唯一程序员而其他人不关心时,这很难。 仍然&#34;轻微泄漏&#34;意味着你的团队中没有人知道程序的实际行为或行为。多线程环境中的非确定性随机行为很容易产生大量随机的Shroeden-bugs,你永远无法重现和修复它们。
您的try-finally
模式确实被破坏了。
您在finally
块中清理的变量应该在try
块之前分配,而不是在其中!
o := TObject.Create;
try
....
finally
o.Destroy;
end;
这是正确的方法:
所以,有时候,
o := nil;
try
o := TObject.Create;
....
finally
o.Free;
end;
这也是正确的。在输入 try-block之前,该变量设置为nil
。如果对象创建失败,那么当finally块调用Free
方法时,变量已经被分配,并且TObject.Free
(但不是TObject.Destroy
)被设计为能够在{{1}上工作对象引用。它本身只是对第一个的嘈杂,过于冗长的修改,但它是少数衍生物的基础。
当您不知道是否要创建对象时,可以使用该模式。
nil
或者当对象创建被延迟时,因为您需要为其创建计算一些数据,或者因为对象非常繁重(例如全局阻止对某个文件的访问),所以您要尽量保持其生命周期尽可能短。
o := nil;
try
...
if SomeConditionCheck()
then o := TObject.Create; // but maybe not
....
finally
o.Free;
end;
该代码虽然问为什么所说的&#34; ...一些代码&#34;没有移到外面和试用块之前。通常它可以而且应该是。一种相当罕见的模式。
创建多个对象时会使用该模式的另一个衍生物;
o := nil;
try
...some code that may raise errors
o := TObject.Create;
....
finally
o.Free;
end;
目标是,如果例如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
并且finally块中的o2
调用将知道它
这是半正确的。假设破坏对象永远不会引发自己的异常。通常这种假设是正确的,但并非总是如此。 无论如何,这种模式允许你将几个try-finally块融合为一个,这使得源代码更短(更容易阅读和推理)并且执行速度更快一些。通常这也是相当安全的,但并非总是如此。
现在两种典型的模式误用:
Free
如果代码BETWEEN对象创建和try-block引发了一些错误 - 则没有任何人可以释放该对象。你刚收到内存泄漏。
当您阅读Delphi资源时,您会看到可能存在类似的模式
o := TObject.Create;
..... some extra code here
try
....
finally
o.Destroy;
end;
对于任何使用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;
行和try-block之间的链接。
当您将来开发该程序时,您可能很容易忘记它并引入错误:比如复制粘贴/将try-block移动到另一个函数中并忘记nil-initializing。或者扩展中间代码并使其使用(因此 - 更改)o := nil
的值。有一种情况我有时会使用它,但这种情况非常罕见并且存在风险。
现在,
o
这是你写的很多,而不考虑尝试 - 终结是如何工作的以及它为何被发明。
问题很简单:当你输入try-block时,你的...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;
变量是一个带有随机垃圾的容器。现在,当您尝试创建对象时,可能会遇到一些错误。然后怎样呢?然后你进入finally块并调用o
- 它该怎么办?它会做随机垃圾。
所以,重复上述所有内容。
(random-garbage).Free
关键字之前分配(初始化)。如果你保护文件 - 然后在try
之前立即打开它。如果你防止内存泄漏 - 在try
之前创建对象。等等。不要在try
运算符之后进行我们的第一次初始化 - 在WITHIN试块中 - 它已经太晚了。try
关键字的正上方初始化(赋值)由try-block保护的变量IMMEDIATELY。更好的是,在分配之前插入一个空行。让它跳进你(或任何其他读者)的眼睛,这个变量和这个尝试是相互依赖的,不应该分开。