Создание / использование 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.
Другие вопросы по тегам