При работе с многопоточностью в Delphi разработчики часто сталкиваются с проблемами утечек памяти, особенно при обработке строк. В этой статье мы разберем конкретный случай, обсуждаемый на форуме, где возникали утечки памяти при параллельном чтении и обработке строк файла, и предложим несколько решений этой проблемы.
Проблема
Исходный код, представленный пользователем Celebr0, демонстрирует попытку параллельной обработки строк файла:
procedure ReadF(const fname: string);
const
BUFSIZE = 1024 * 18;
var
Line: string;
List: TStringList;
Reader: TStreamReader;
Buf: array [0..BUFSIZE] of char;
begin
List := TStringList.Create;
Reader := TStreamReader.Create(fname, TEncoding.Default, True, 4096);
try
while not Reader.EndOfStream do
begin
Reader.ReadBlock(@Buf, 0, BUFSIZE);
List.Text := Buf;
parallel.&For(0, List.Count - 1).Execute(
procedure(value: integer)
begin
Line := List[value];
end);
end;
finally
List.Free;
Reader.Close;
FreeAndNil(Reader);
end;
end;
Основные проблемы в этом коде:
Небезопасное присваивание строки в многопоточной среде: Переменная Line используется всеми потоками одновременно
Проблемы с управлением памятью строк: Строки в Delphi используют подсчет ссылок, который не является атомарной операцией
Неправильное завершение потоков: Использование WaitForSingleObject может приводить к утечкам
Почему возникают утечки памяти
Как правильно отметили участники обсуждения (Dalija Prasnikar, Tommi Prami и другие), основная причина утечек - небезопасная работа со строками в многопоточной среде.
Строки в Delphi: - Являются ссылочными типами - Используют механизм подсчета ссылок - Операции с ними не являются атомарными
Когда несколько потоков одновременно пытаются изменить одну строковую переменную (Line в данном случае), это может привести: - К некорректному подсчету ссылок - К утечкам памяти - К возможным аварийным завершениям программы
Решения проблемы
1. Использование локальных переменных в каждом потоке
Самый простой способ избежать проблем - использовать отдельную переменную для каждого потока:
procedure ReadF(const fname: string);
const
BUFSIZE = 1024 * 18;
var
List: TStringList;
Reader: TStreamReader;
Buf: array [0..BUFSIZE] of char;
begin
List := TStringList.Create;
Reader := TStreamReader.Create(fname, TEncoding.Default, True, 4096);
try
while not Reader.EndOfStream do
begin
Reader.ReadBlock(@Buf, 0, BUFSIZE);
List.Text := Buf;
parallel.&For(0, List.Count - 1).NoWait.Execute(
procedure(value: integer)
var
LocalLine: string; // Локальная переменная для каждого потока
begin
LocalLine := List[value];
// Обработка LocalLine
end);
end;
finally
List.Free;
Reader.Close;
FreeAndNil(Reader);
end;
end;
2. Использование критических секций
Если необходимо использовать общую переменную, нужно защитить доступ к ней:
var
CS: TCriticalSection;
procedure ReadF(const fname: string);
const
BUFSIZE = 1024 * 18;
var
Line: string;
List: TStringList;
Reader: TStreamReader;
Buf: array [0..BUFSIZE] of char;
begin
CS := TCriticalSection.Create;
try
List := TStringList.Create;
Reader := TStreamReader.Create(fname, TEncoding.Default, True, 4096);
try
while not Reader.EndOfStream do
begin
Reader.ReadBlock(@Buf, 0, BUFSIZE);
List.Text := Buf;
parallel.&For(0, List.Count - 1).NoWait.Execute(
procedure(value: integer)
begin
CS.Enter;
try
Line := List[value];
// Обработка Line
finally
CS.Leave;
end;
end);
end;
finally
List.Free;
Reader.Close;
FreeAndNil(Reader);
end;
finally
CS.Free;
end;
end;
3. Альтернативный подход с TParallel.For
В современных версиях Delphi можно использовать встроенный TParallel.For:
uses
System.Threading;
procedure ReadF(const fname: string);
const
BUFSIZE = 1024 * 18;
var
List: TStringList;
Reader: TStreamReader;
Buf: array [0..BUFSIZE] of char;
begin
List := TStringList.Create;
Reader := TStreamReader.Create(fname, TEncoding.Default, True, 4096);
try
while not Reader.EndOfStream do
begin
Reader.ReadBlock(@Buf, 0, BUFSIZE);
List.Text := Buf;
TParallel.For(0, List.Count - 1,
procedure(value: integer)
var
LocalLine: string;
begin
LocalLine := List[value];
// Обработка LocalLine
end);
end;
finally
List.Free;
Reader.Close;
FreeAndNil(Reader);
end;
end;
4. Решение с использованием OmniThreadLibrary
Для более сложных сценариев можно использовать библиотеку OmniThreadLibrary:
uses
OtlParallel;
procedure ReadF(const fname: string);
const
BUFSIZE = 1024 * 18;
var
List: TStringList;
Reader: TStreamReader;
Buf: array [0..BUFSIZE] of char;
begin
List := TStringList.Create;
Reader := TStreamReader.Create(fname, TEncoding.Default, True, 4096);
try
while not Reader.EndOfStream do
begin
Reader.ReadBlock(@Buf, 0, BUFSIZE);
List.Text := Buf;
Parallel.ForEach(0, List.Count - 1).NoWait.Execute(
procedure(const value: integer)
var
LocalLine: string;
begin
LocalLine := List[value];
// Обработка LocalLine
end);
end;
finally
List.Free;
Reader.Close;
FreeAndNil(Reader);
end;
end;
Почему NoWait решает проблему
Как заметил Celebr0, использование .NoWait в OmniThreadLibrary решает проблему утечек. Это происходит потому, что:
Без .NoWait главный поток ожидает завершения всех рабочих потоков
При этом могут возникать взаимоблокировки и проблемы с освобождением ресурсов
.NoWait позволяет потокам завершаться асинхронно, что в данном случае предотвращает утечки
Однако, это не решает коренную проблему небезопасного доступа к строкам, а лишь маскирует ее.
Рекомендации по многопоточной работе со строками
Избегайте разделяемых строковых переменных: По возможности используйте локальные переменные в каждом потоке
Защищайте доступ к общим данным: Используйте критические секции, мьютексы или другие механизмы синхронизации
Проверяйте код на утечки: Используйте такие инструменты как FastMM или ReportMemoryLeaksOnShutdown
Используйте современные конструкции: В новых версиях Delphi есть встроенные средства для параллельного программирования
Избегайте блокировки главного потока: Используйте асинхронные подходы, особенно в UI-приложениях
Заключение
Проблема утечек памяти при параллельной обработке строк - классический пример сложностей многопоточного программирования. Как показало обсуждение, даже опытные разработчики могут допускать ошибки в этой области.
Лучшим решением является использование локальных переменных в каждом потоке и современных библиотек для параллельного программирования. Если же необходимо использовать общие данные, обязательно защищайте доступ к ним с помощью соответствующих механизмов синхронизации.
Помните, что многопоточное программирование требует особой внимательности и тщательного тестирования кода.
Статья описывает методы предотвращения утечек памяти при многопоточной обработке строк в Delphi, включая использование локальных переменных, критических секций и современных библиотек параллельного программирования.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.