TStringList и TThread, которые не освобождают всю свою память

Используемая версия: Delphi 7.

Я работаю над программой, которая выполняет простой цикл for в Virtual ListView. Данные хранятся в следующей записи:

type TList=record
  Item:Integer;
  SubItem1:String;
  SubItem2:String;
end;

Элемент — индекс. SubItem1 статус операций (успешно или нет). SubItem2 — путь к файлу. Цикл for загружает каждый файл, выполняет несколько операций, а затем сохраняет его. Операции происходят в TStringList. Файлы около 2мб каждый.

Теперь, если я выполняю операции в основной форме, все работает отлично.

Многопоточность, есть огромная проблема с памятью. Почему-то TStringList не кажется полностью освобожденным. После 3-4k файлов я получаю исключение EOutofMemory. Иногда софт зависает на 500-600мб, иногда нет. В любом случае TStringList всегда возвращает исключение EOutofMemory, и файл больше не может быть загружен. На компьютерах с большим объемом памяти получение исключения занимает больше времени.

То же самое происходит и с другими компонентами. Например, если я использую THTTPSend из Synapse, через некоторое время программа не сможет создавать новые потоки, потому что потребление памяти слишком велико. Это около 500-600 МБ, а должно быть максимум 100 МБ. На главной форме все работает нормально.

Думаю, ошибка на моей стороне. Возможно, я недостаточно понимаю темы. Я попытался освободить все в событии Destroy. Я попробовал процедуру FreeAndNil. Я пробовал только с одним потоком за раз. Я попытался освободить поток вручную (без FreeOnTerminate...)

Не повезло.

Итак, вот код потока. Это только основная идея; не полный код со всеми операциями. Если я удаляю процедуру LoadFile, все работает хорошо. Поток создается для каждого файла в соответствии с пулом потоков.

unit OperationsFiles;

interface

uses Classes, SysUtils, Windows;

type
 TOperationFile = class(TThread)
 private
  Position : Integer;
  TPath, StatusMessage: String;
  FileStringList: TStringList;
  procedure UpdateStatus;
  procedure LoadFile;
 protected
  procedure Execute; override;
 public
  constructor Create(Path: String; LNumber: Integer);
 end;

implementation

uses Form1;

procedure TOperationFile.LoadFile;
begin
 try
  FileStringList.LoadFromFile(TPath);
  // Operations...
  StatusMessage := 'Success';
 except
  on E : Exception do StatusMessage := E.ClassName;
 end;
end;

constructor TOperationFile.Create(Path : String; LNumber: Integer);
begin
 inherited Create(False);
 TPath := Path;
 Position := LNumber;
 FreeOnTerminate := True;
end;

procedure TOperationFile.UpdateStatus;
begin
 FileList[Position].SubItem1 := StatusMessage;
 Form1.ListView4.UpdateItems(Position,Position);
end;

procedure TOperationFile.Execute;
begin
 FileStringList:= TStringList.Create;
 LoadFile;

 Synchronize(UpdateStatus);

 FileStringList.Free;
end;

end.

В чем может быть проблема?

В какой-то момент я подумал, что, может быть, создается слишком много потоков. Если пользователь загружает 1 миллион файлов, в конечном итоге будет создан 1 миллион потоков, хотя одновременно создаются и выполняются только 50 потоков.

Спасибо за ваш вклад.


person Pascal Bergeron    schedule 25.06.2012    source источник
comment
@TLama Уже сделал это ... и, согласно FastMM, утечки памяти нет, кроме тех, что из Delphi 7.   -  person Pascal Bergeron    schedule 25.06.2012
comment
@TLama 13–20 байт: AnsiString x 1 29–36 байт: Unknown x 1 45–52 байта: TStringList x 2 Я считаю, что это утечки памяти из самого Delphi. Поправьте меня если я ошибаюсь.   -  person Pascal Bergeron    schedule 25.06.2012
comment
Я никогда не сталкивался с утечками памяти из самой Delphi.   -  person Jerry Dodge    schedule 25.06.2012
comment
Fastmm может предоставить стек вызовов утечки памяти, убедитесь, что у вас установлена ​​полная версия Fastmm. Кроме того, вы не показали, как заполняется FileList. Я подозреваю, что код вашего потока генерирует исключение, которое вы не обрабатываете, в обход вашего вызова TStringList.Free(). Либо добавьте блок try/finally, либо переопределите DoTerminate(), чтобы Free() всегда вызывался.   -  person Remy Lebeau    schedule 25.06.2012
comment
@Jerry Утечка RTL/VCL в старых версиях Delphis. Emba начала исправлять эти утечки, когда они начали использовать FastMM.   -  person David Heffernan    schedule 25.06.2012
comment
Целью пула потоков является повторное использование потоков, поскольку создание/уничтожение потоков является тяжелой задачей для системы. Я подозреваю, что причиной вашего сбоя кода может быть то, как вы обрабатываете пул.   -  person LU RD    schedule 25.06.2012
comment
@LURD Если между потоками и заданиями существует взаимно однозначное сопоставление, то это не пул в истинном значении этого слова, что, я думаю, является вашей точкой зрения ;-)   -  person David Heffernan    schedule 25.06.2012
comment
Я не вижу раздела try finally для FileStringList, поэтому возможна утечка...   -  person whosrdaddy    schedule 25.06.2012
comment
@whosrdaddy Было бы лучше с try/finally, но поведение TOperationFile.LoadFile при проглатывании исключений фактически означает, что он не приведет к утечке списка строк.   -  person David Heffernan    schedule 25.06.2012
comment
@DavidHeffernan, а что, если updatestatus не удастся?? (например, неверная позиция)?? В любом случае, он не показывает настоящий код, так что мы можем гоняться здесь за призраками...   -  person whosrdaddy    schedule 25.06.2012
comment
Именно то, что я думал. Если во время выполнения Synchronize() в основном потоке возникает исключение, Synchronize() перехватывает и повторно вызывает его в контексте потока, вызвавшего Synchronize(). Таким образом, LoadFile() может перехватывать исключения, а UpdateStatus() — нет, что может привести к тому, что исключение будет передано обратно в Execute() и пропустит вызов FileStringList.Free().   -  person Remy Lebeau    schedule 26.06.2012


Ответы (2)


В коде, который вы показываете в вопросе, (вероятно) нет утечек.

Я говорю, вероятно, потому что исключение, вызванное во время Execute, может привести к утечке. Время жизни списка строк должно быть защищено блоком finally.

FileStringList:= TStringList.Create;
try
  LoadFile;
  Synchronize(UpdateStatus);
finally
  FileStringList.Free;
end;

Тем не менее, я ожидаю, что проглатывание исключений в LoadFile означает, что вы не пропускаете список строк.

Вы говорите, что, возможно, создаются тысячи потоков. Каждый поток резервирует память для своего стека, а размер стека по умолчанию составляет 1 МБ. Когда у вас зарезервированы тысячи стеков по 1 МБ, вы можете легко исчерпать или фрагментировать адресное пространство.

Я видел проблемы из-за бесцеремонного создания тем в прошлом. Например, у меня была программа, которая давала сбой при создании и уничтожении потоков, при этом никогда не существовало более 256 потоков. Это было на 16-ядерной машине с адресным пространством 4 ГБ. Вероятно, у вас есть 2 ГБ доступного адресного пространства.

Хотя вы заявляете, что в любой момент существует не более 50 потоков, я не уверен, как вы можете быть в этом уверены. Не в последнюю очередь потому, что вы установили FreeOnTerminate в True и тем самым отказались от контроля над временем жизни ваших потоков.

Я предполагаю, что ваши проблемы связаны с количеством создаваемых вами потоков. Одного потока на процессор будет достаточно. Повторно используйте свои темы. Создание и уничтожение потока для небольшой задачи обходится дорого.

Если этого недостаточно для решения ваших проблем, вам нужно будет показать код, который управляет временем жизни потока.

Наконец, мне интересно, сколько пользы вы извлечете из этого приложения. Если он связан с вводом-выводом, то многопоточная версия может быть медленнее!

person David Heffernan    schedule 25.06.2012
comment
Я думал так же, но посмотрите на обновление вопроса одновременно создается и работает только 50 потоков. - person TLama; 25.06.2012
comment
@TLama Что-то не сходится. Код в Q не протекает. Создание слишком большого количества потоков — это простой способ съесть все ваше виртуальное адресное пространство. - person David Heffernan; 25.06.2012
comment
@DavidHeffernan - В каком режиме отказа страдала эта программа? (Создание/удаление потоков, не более 256?). Сейчас я борюсь с похожей проблемой - система реального времени, которая отбирает данные с некоторого оборудования примерно 10 раз в секунду. Сторонний драйвер (по какой-либо причине) создает и уничтожает поток для каждого измерения (каждое занимает около 10 мс). После дня или около того непрерывной работы с циклом ~ 36000 потоков в час он увязает в ползании. В остальном программа плотная и скудная. Интересно, какой у вас был опыт. - person J...; 25.06.2012
comment
@J ... Я на самом деле не помню точного режима отказа. Определенно, один из вызовов Win32 API не удался, что привело к исключению Delphi во время выполнения. Но я не могу вспомнить, какой вызов API не удался. - person David Heffernan; 25.06.2012
comment
@DavidHeffernan достаточно честно ... подумал, что не мешало бы спросить. В любом случае, это дало мне еще кое-что для расследования. - person J...; 25.06.2012
comment
Что ж, я попытаюсь повторно использовать свои темы, чтобы увидеть, работает ли это. Я скажу вам позже мои результаты. Что касается количества запущенных потоков, то оно не отображается в коде, но есть счетчик потоков. Когда поток завершает свою работу, счетчик уменьшается, чтобы можно было создать новый поток. Моя программа выполняет множество операций. Я согласен с тем, что пример TStringList может быть не самым лучшим, но мне было проще всего показать его в псевдокоде. - person Pascal Bergeron; 25.06.2012

На основании предоставленной информации невозможно воспроизвести вашу ошибку. Некоторые подсказки сделаны Реми и Дэвидом, которые могут вам помочь.

Глядя на структуру вашей программы, поток можно разделить на два классических решения.

Первая часть, в которой вы делегируете задачи разным потокам, — это проблема Single-Producer-Multiple-Consumer. Здесь ее можно решить, создав небольшое количество потоков, передав им потокобезопасную очередь объектов. Затем основной поток помещает объекты задачи в очередь. Потоки-потребители заботятся об отдельных задачах проверки файлов.

Вторая часть, где результат должен быть передан в основной поток, является Multiple-Producer-Single-Consumer проблемой. Если вы передадите вторую потокобезопасную очередь объектов потокам при инициализации, они смогут легко поместить результаты в очередь. Слить очередь результатов из основного потока в рамках события таймера.

person LU RD    schedule 25.06.2012