Странное исключение EOutOfMemory с использованием TStringList
У меня есть система, которая загружает некоторые текстовые файлы, которые заархивированы в файл ".log" и затем разбираются в информационных классах, используя несколько потоков, каждый из которых имеет дело с отдельным файлом, и добавляет проанализированные объекты в список. Файл загружается с использованием TStringList, так как это был самый быстрый метод, который я тестировал.
Количество текстовых файлов варьируется, но обычно мне приходится иметь дело с чем-то от 5 до 8 файлами в диапазоне от 50 МБ до 120 МБ за одно вторжение.
Моя проблема: пользователь может загружать файлы.log столько раз, сколько пожелает, и после некоторых из этих процессов я получаю исключение EOutOfMemory при попытке использовать TStringList.LoadFromFile. Конечно, первое, что приходит на ум любому, кто когда-либо использовал StringList, это то, что вы не должны использовать его при работе с большими текстовыми файлами, но это исключение происходит случайным образом и после того, как процесс уже был успешно завершен хотя бы один раз (объекты уничтожаются до начала нового анализа, поэтому память извлекается правильно, за исключением незначительных утечек)
Я пытался использовать текстиль и 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.
Добавлено: Спасибо за все ваши отзывы. Я коснусь некоторых вопросов, которые обсуждались, но я не упомянул в своем первоначальном вопросе.
Файл.log содержит несколько экземпляров файлов.txt внутри, но также может иметь несколько файлов.log, каждый файл представляет дневную стоимость ведения журнала или период, выбранный пользователем, поскольку распаковка занимает много времени при запуске потока каждый раз, когда обнаруживается.txt, так что я могу немедленно начать синтаксический анализ, это сокращает заметное время ожидания для пользователя
"Незначительные утечки" не отображаются в ReportMemoryLeaksOnShutdown, и другие методы, такие как TStreamReader, позволяют избежать этой проблемы.
Список команд хранится в TLogFile. Существует только один экземпляр этого класса в любое время, и он уничтожается всякий раз, когда пользователь хочет загрузить файл.log. Все потоки добавляют команды к одному и тому же объекту, вот причина критического раздела.
Не могу детализировать процесс разбора, так как он раскрыл бы некоторую разумную информацию, но это простой сбор информации из строки и команды TCommand.
С самого начала я знал о фрагментации, но я не нашел конкретного доказательства того, что TStringList вызывает фрагментацию только при многократной загрузке, если это можно подтвердить, я был бы очень рад
Спасибо за внимание. Я использовал внешнюю библиотеку, которая была способна читать строки и загружать файлы с той же скоростью, что и TStringList
без необходимости загружать весь файл в память
1 ответ
TStringList
медленный класс как таковой. У этого есть много наворотов - дополнительные функции и функции, которые затормаживают его. Гораздо быстрее будут контейнерыTList<String>
или просто старая динамикаarray of string
, УвидетьSystem.IOUTils.TFile.ReadAllLines
функция.Прочитайте о фрагментации памяти кучи, например, http://en.wikipedia.org/Heap_fragmentation
Это может произойти и сломать ваше приложение даже без утечек памяти. Но так как вы говорите, что есть много небольших утечек - это то, что, скорее всего, произойдет. Вы можете более или менее отложить сбой, избегая чтения целых файлов в память и работая с меньшими порциями. Но деградация будет продолжаться, даже медленнее, и в конце концов ваша программа снова рухнет.
- Есть много специальных библиотек классов, читающих большие файлы по частям с буферизацией, предварительной выборкой и чем-то другим. Одной из таких библиотек, ориентированных на тексты, является http://github.com/d-mozulyov/CachedTexts а также есть и другие.
PS. Главные примечания.
Я думаю, что ваша команда должна пересмотреть, сколько вам нужно многопоточности. Честно говоря, я не вижу никого. Вы загружаете файлы с жесткого диска и, вероятно, записываете обработанные и преобразованные файлы на тот же (в лучшем случае, на другой) жесткий диск. Это означает, что скорость вашей программы ограничена скоростью диска. И эта скорость НАМНОГО меньше, чем скорости процессора и оперативной памяти. Вводя многопоточность, вы, кажется, только делаете свою программу более сложной и хрупкой. Ошибки гораздо труднее обнаружить, хорошо известные библиотеки могут неожиданно работать неправильно в режиме MT и т. Д. И вы, вероятно, не получите никакого увеличения производительности, поскольку узкое место связано со скоростью ввода-вывода диска.
Если вы все еще хотите многопоточность ради этого - тогда, возможно, загляните в OmniThreading Library. Он был разработан, чтобы упростить разработку "потоков данных" типов приложений MT. Прочитайте учебные пособия и примеры.
Я определенно предлагаю вам устранить все эти "мелкие утечки" и как часть этого исправить все предупреждения компиляции. Я знаю, это тяжело, когда ты не единственный программист в проекте, а другим все равно. Тем не менее, "незначительные утечки" означают, что никто в вашей команде не знает, как на самом деле ведет себя программа. А недетерминированное случайное поведение в многопоточной среде может легко генерировать тонны случайных ошибок Шредена, которые вы никогда не сможете воспроизвести и исправить.
Ваш try-finally
образец действительно сломан. Переменная, в которой вы убираете finally
блок должен быть назначен прямо перед try
блок, не в этом!
o := TObject.Create;
try
....
finally
o.Destroy;
end;
Это правильный путь:
- либо объект не может быть создан - тогда блок try не будет введен, и не будет блок finally.
- или объект успешно создан - и тогда будет введен try-block, а затем будет finally-block
Так что иногда
o := nil;
try
o := TObject.Create;
....
finally
o.Free;
end;
Это тоже правильно. Переменная установлена в nil
непосредственно перед вводом try-block. Если создание объекта не удается, тогда когда finally-блокирует вызовы Free
метод, переменная уже была назначена, и TObject.Free
(но нет TObject.Destroy
) был разработан, чтобы иметь возможность работать на nil
ссылки на объекты. Сама по себе является просто шумной, слишком многословной модификацией первой, но она служит основой для еще нескольких производных.
Этот шаблон может использоваться, когда вы не знаете, создадите ли вы объект или нет.
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 знают это.
Это полу правильно. Предполагается, что разрушение объектов никогда не вызовет собственных исключений. Обычно это предположение верно, но не всегда. В любом случае, этот шаблон позволяет объединить несколько блоков try-finally в один, что делает исходный код короче (его легче читать и рассуждать), а выполнение немного быстрее. Обычно это также достаточно безопасно, но не всегда.
Теперь два типичных неправильных использования шаблона:
o := TObject.Create;
..... some extra code here
try
....
finally
o.Destroy;
end;
Если код между созданием объекта и блоком try вызывает некоторую ошибку - тогда нет никого, кто мог бы освободить этот объект. Вы только что получили утечку памяти.
Когда вы читаете источники 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 в другую функцию и забвение инициализации 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
- а что он должен делать? Это сделало бы случайный мусор.
Итак, повторим все вышесказанное.
- try-finally используется для гарантии освобождения объектов или любых других переменных (закрытие файлов, закрытие окон и т. д.), и, следовательно,:
- переменная, используемая для отслеживания этого ресурса (например, ссылки на объект), должна иметь хорошо известное значение при входе в блок try, это должно быть присвоено (инициализировано) до
try
ключевое слово. Если вы охраняете файл - откройте его непосредственно передtry
, Если вы остерегаетесь утечки памяти - создайте объект раньшеtry
, И т.д. Не делайте нашу первую инициализацию ПОСЛЕtry
оператор - ВНУТРИ блок-поста - там слишком поздно. - вам лучше спроектировать код настолько простым (самоочевидным), насколько это возможно, исключив возможность появления будущих ошибок, когда вы забудете неявные скрытые предположения, которые вы сегодня держите в уме, - и будете их пересекать. Видите, кто написал эту программную поговорку? "Всегда пишите код, как будто парень, который в конечном итоге будет поддерживать ваш код, будет жестоким психопатом, который знает, где вы живете"., Здесь это означает, инициализировать (назначить) переменную, защищаемую блоком try, НЕМЕДЛЕННО перед началом блока, прямо над
try
ключевое слово. Более того, вставьте пустую строку перед этим назначением. Сделайте так, чтобы вы (или любой другой читатель) увидели, что эта переменная и эта попытка взаимозависимы и никогда не должны разбиваться на части.