Создание / использование FileStream Thread Safe
В моем приложении, когда я пишу текстовые файлы (журналы, следы и т. Д.), Я использую TFileStream
учебный класс. Есть случаи, когда я записываю данные в многопоточную среду, это шаги:
1- Запись данных в кэш
2- Для каждой 1000 строк я сохраняю в файл.
3- Очистить данные.
Этот процесс повторяется в течение всей обработки.
Описание проблемы:
С 16 потоками система выдает следующее исключение:
Нарушение прав доступа - файл, который уже используется другим приложением.
Я предполагаю, что это происходит потому, что дескриптор, используемый одним потоком, еще не закрыт, когда нужно открыть другой поток.
Я изменил архитектуру следующим образом: (ниже - НОВАЯ реализация)
В предыдущем случае TFileStream был создан с параметрами FileName и Mode и уничтожил закрытие дескриптора (я не использовал TMyFileStream)
TMyFileStream = class(TFileStream)
public
destructor Destroy; override;
end;
TLog = class(TStringList)
private
FFileHandle: Integer;
FirstTime: Boolean;
FName: String;
protected
procedure Flush;
constructor Create;
destructor Destroy;
end;
destructor TMyFileStream.Destroy;
begin
//Do Not Close the Handle, yet!
FHandle := -1;
inherited Destroy;
end;
procedure TLog.Flush;
var
StrBuf: PChar; LogFile: string;
F: TFileStream;
InternalHandle: Cardinal;
begin
if (Text <> '') then
begin
LogFile:= GetDir() + FName + '.txt';
ForceDirectories(ExtractFilePath(LogFile));
if FFileHandle < 0 then
begin
if FirstTime then
FirstTime := False;
if FileExists(LogFile) then
if not SysUtils.DeleteFile(LogFile) then
RaiseLastOSError;
InternalHandle := CreateFile(PChar(LogFile), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_NEW, 0,0);
if InternalHandle = INVALID_HANDLE_VALUE then
RaiseLastOSError
else if GetLastError = ERROR_ALREADY_EXISTS then
begin
InternalHandle := CreateFile(PChar(LogFile), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_EXISTING, 0,0);
if InternalHandle = INVALID_HANDLE_VALUE then
RaiseLastOSError
else
FFileHandle := InternalHandle;
end
else
FFileHandle := InternalHandle;
end;
F := TMyFileStream.Create(FFileHandle);
try
StrBuf := PChar(Text);
F.Position := F.Size;
F.Write(StrBuf^, StrLen(StrBuf));
finally
F.Free();
end;
Clear;
end;
end;
destructor TLog.Destroy;
begin
FUserList:= nil;
Flush;
if FFileHandle >= 0 then
CloseHandle(FFileHandle);
inherited;
end;
constructor TLog.Create;
begin
inherited;
FirstTime := True;
FFileHandle := -1;
end;
Есть еще лучший способ?
Это правильная реализация?
Могу ли я улучшить это?
Мое предположение о ручке было правильным?
Все theads используют один и тот же объект журнала.
Входа нет, я проверил! что-то не так с TFileStream.
Доступ к надстройке синхронизирован, я имею в виду, я использовал критическую сессию, и когда она достигает 1000 строк, вызывается процедура Flush.
PS: я не хочу сторонний компонент, я хочу создать свой собственный.
5 ответов
Ну, для начала, нет смысла TMyFileStream
, То, что вы ищете, это THandleStream
, Этот класс позволяет вам указать дескриптор файла, время жизни которого вы контролируете. И если вы используете THandleStream
вы сможете избежать довольно неприятных хаков вашего варианта. Тем не менее, почему вы вообще беспокоитесь о потоке? Замените код, который создает и использует поток, вызовом SetFilePointer
искать в конец файла, и вызов WriteFile
писать контент.
Однако, даже используя это, предложенное вами решение требует дальнейшей синхронизации. Один дескриптор файла Windows не может использоваться одновременно из нескольких потоков без синхронизации. В комментарии (должно быть в вопросе) вы намекаете, что сериализуете записи в файл. Если так, то у тебя все хорошо.
Многопоточное решение, предоставленное Марко Пауновичем, довольно приятно, однако, просматривая код, я заметил небольшую ошибку, возможно, просто недосмотр в примере, но я подумал, что упомяну это точно так же, если кто-то на самом деле пытается использовать его как есть.,
В TLogger.Destroy отсутствует вызов Flush, в результате чего любые неразбавленные (буферизованные) данные игнорируются при уничтожении объекта TLogger.
destructor TLogger.Destroy;
begin
if FStrings.Count > 0 then
Flush;
FStrings.Free;
DeleteCriticalSection(FLock);
inherited;
end;
Как насчет:
В каждом потоке добавляйте строки журнала в экземпляр TStringList, пока не будет lines.count=1000. Затем поместите TStringList в блокирующую очередь производителя-потребителя, немедленно создайте новый TStringList и продолжите запись в новый список.
Используйте один поток ведения журнала, который удаляет из очереди экземпляры TStringList, записывает их в файл и затем освобождает их.
Это изолирует записи в журнале от задержек на диске / сети, удаляет любую хитрую блокировку файлов и фактически будет работать надежно.
Я понял, МОЯ ОШИБКА.
Во-первых, я хочу извиниться за публикацию этого глупого вопроса без надлежащего способа воспроизвести исключение. Другими словами, без SSCCE.
Проблема была в контрольном флаге, который мой TLog
класс используется внутренне.
Этот флаг был создан, когда мы начали развивать наш продукт параллельной архитектурой.
Поскольку нам нужно было сохранить прежнюю форму в рабочем состоянии (по крайней мере, пока все не было в новой архитектуре). Мы создали несколько флагов, чтобы определить, была ли объект новой или старой версией. Один из этих флагов был назван CheckMaxSize
,
Если CheckMaxSize
Было включено, что в определенный момент все данные внутри экземпляра этого объекта в каждом потоке будут выбрасываться в основной экземпляр, который находится в "основном" потоке (а не в графическом интерфейсе, поскольку это была фоновая работа). Кроме того, когда CheckMaxSize
включен, TLog никогда не должен вызывать "сброс".
Наконец, как вы можете видеть, в TLog.Destroy
нет чека на CheckMaxSize
, Следовательно, проблема может возникнуть из-за того, что имя файла, созданного этим классом, всегда было одинаковым, поскольку он обрабатывал одну и ту же задачу, и когда один объект создал файл, а другой попытался создать другой файл с таким же именем внутри Исключение - та же папка, ОС (Windows).
Решение:
Перепишите деструктор, чтобы:
destructor TLog.Destroy;
begin
if CheckMaxSize then
Flush;
if FFileHandle >= 0 then
CloseHandle(FFileHandle);
inherited;
end;
Если у вас есть многопоточный код, который нужно записать в один файл, лучше иметь столько контроля, сколько сможете. А это значит, избегайте занятий, в которых вы не уверены на 100%, как они работают.
Я предлагаю вам использовать несколько потоков> архитектуру единого регистратора, где каждый поток будет иметь ссылку на объект регистратора, и добавлять к нему строки. Как только 1000 строк будут достигнуты, регистратор сбросит собранные данные в файл.
- Нет необходимости использовать TFileStream для записи данных в файл, вы можете использовать CreateFile()/SetFilePointer()/WriteFile(), как Дэвид уже предлагал
- TStringList не является потокобезопасным, поэтому вы должны использовать блокировки на нем
main.dpr:
{$APPTYPE CONSOLE}
uses
uLogger,
uWorker;
const
WORKER_COUNT = 16;
var
worker: array[0..WORKER_COUNT - 1] of TWorker;
logger: TLogger;
C1 : Integer;
begin
Write('Creating logger...');
logger := TLogger.Create('test.txt');
try
WriteLn(' OK');
Write('Creating threads...');
for C1 := Low(worker) to High(worker) do
begin
worker[C1] := TWorker.Create(logger);
worker[C1].Start;
end;
WriteLn(' OK');
Write('Press ENTER to terminate...');
ReadLn;
Write('Destroying threads...');
for C1 := Low(worker) to High(worker) do
begin
worker[C1].Terminate;
worker[C1].WaitFor;
worker[C1].Free;
end;
WriteLn(' OK');
finally
Write('Destroying logger...');
logger.Free;
WriteLn(' OK');
end;
end.
uWorker.pas:
unit uWorker;
interface
uses
System.Classes, uLogger;
type
TWorker = class(TThread)
private
FLogger: TLogger;
protected
procedure Execute; override;
public
constructor Create(const ALogger: TLogger);
destructor Destroy; override;
end;
implementation
function RandomStr: String;
var
C1: Integer;
begin
result := '';
for C1 := 10 to 20 + Random(50) do
result := result + Chr(Random(91) + 32);
end;
constructor TWorker.Create(const ALogger: TLogger);
begin
inherited Create(TRUE);
FLogger := ALogger;
end;
destructor TWorker.Destroy;
begin
inherited;
end;
procedure TWorker.Execute;
begin
while not Terminated do
FLogger.Add(RandomStr);
end;
end.
uLogger.pas:
unit uLogger;
interface
uses
Winapi.Windows, System.Classes;
type
TLogger = class
private
FStrings : TStringList;
FFileName : String;
FFlushThreshhold: Integer;
FLock : TRTLCriticalSection;
procedure LockList;
procedure UnlockList;
procedure Flush;
public
constructor Create(const AFile: String; const AFlushThreshhold: Integer = 1000);
destructor Destroy; override;
procedure Add(const AString: String);
property FlushThreshhold: Integer read FFlushThreshhold write FFlushThreshhold;
end;
implementation
uses
System.SysUtils;
constructor TLogger.Create(const AFile: String; const AFlushThreshhold: Integer = 1000);
begin
FFileName := AFile;
FFlushThreshhold := AFlushThreshhold;
FStrings := TStringList.Create;
InitializeCriticalSection(FLock);
end;
destructor TLogger.Destroy;
begin
FStrings.Free;
DeleteCriticalSection(FLock);
inherited;
end;
procedure TLogger.LockList;
begin
EnterCriticalSection(FLock);
end;
procedure TLogger.UnlockList;
begin
LeaveCriticalSection(FLock);
end;
procedure TLogger.Add(const AString: String);
begin
LockList;
try
FStrings.Add(AString);
if FStrings.Count >= FFlushThreshhold then
Flush;
finally
UnlockList;
end;
end;
procedure TLogger.Flush;
var
strbuf : PChar;
hFile : THandle;
bWritten: DWORD;
begin
hFile := CreateFile(PChar(FFileName), GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
try
strbuf := PChar(FStrings.Text);
SetFilePointer(hFile, 0, nil, FILE_END);
WriteFile(hFile, strbuf^, StrLen(strbuf), bWritten, nil);
FStrings.Clear;
finally
CloseHandle(hFile);
end;
end;
end.