创建/使用FileStream线程安全

时间:2013-05-14 12:50:16

标签: multithreading delphi file-io race-condition delphi-6

在我的应用程序中,当我写文本文件(日志,跟踪等)时,我使用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;

还有另一种更好的方法吗?
这个实现是否正确?
我可以改善吗?
我对Handle的猜测是对的?

所有theads都使用相同的Log对象。

没有重入,我检查了! TFileStream有问题。

对Add的访问是同步的,我的意思是,我使用了关键会话,当它达到1000行时,调用Flush过程。

P.S:我不想要第三方组件,我想创建自己的组件。

5 个答案:

答案 0 :(得分:1)

嗯,首先,TMyFileStream没有意义。您要找的是THandleStream。该类允许您提供您控制其生命周期的文件句柄。如果你使用THandleStream,你将能够避免你的变种相当讨厌的黑客。那说,为什么你甚至打扰流?将创建和使用流的代码替换为SetFilePointer调用以寻找文件的末尾,并调用WriteFile来编写内容。

但是,即使使用它,您提出的解决方案也需要进一步同步。没有同步,不能从多个线程同时使用单个Windows文件句柄。您提示您正在序列化文件写入的注释(应该在问题中)。如果是这样,那你就没事了。

答案 1 :(得分:1)

Marko Paunovic提供的线程解决方案相当不错,但是在查看代码时我注意到了一个小错误,或许只是在示例中的疏忽但我认为我会提到它只是在有人实际尝试使用它时原样。

在TLogger.Destroy中缺少对Flush的调用,因此当TLogger对象被销毁时,任何未刷新(缓冲)的数据都会被拒绝。

destructor TLogger.Destroy;
begin
  if FStrings.Count > 0 then
     Flush;

  FStrings.Free;
  DeleteCriticalSection(FLock);

  inherited;
end;

答案 2 :(得分:0)

怎么样:

在每个线程中,将日志行添加到TStringList实例,直到lines.count = 1000。然后将TStringList推送到阻塞的生产者 - 消费者队列,立即创建一个新的TStringList并继续记录到新列表。

使用一个Logging线程将TStringList实例出列,将它们写入文件然后释放它们。

这将日志写入与磁盘/网络延迟隔离开来,消除了对狡猾的文件锁定的依赖,并且实际上可靠地工作。

答案 3 :(得分:0)

我认为 MY MISTAKE

首先,我想在没有正确方法重现异常的情况下发布这个愚蠢的问题而道歉。换句话说,没有SSCCE。

问题是我的TLog类内部使用的控制标志。

当我们开始将产品发展为并行架构时,就创建了这个标志。

因为我们需要保持上一个表单的工作(至少在所有内容都在新架构中之前)。 我们创建了一些标志来标识对象是新版本还是旧版本。 其中一个标志名为CheckMaxSize

如果CheckMaxSize已启用,则在某个时刻,每个线程中此对象的实例内的每个数据都将被抛出到主实例中,该实例位于“主”线程中(不是GUI主线程) ,因为这是一个背景工作)。此外,当启用CheckMaxSize时,TLog永远不应该调用“flush”。

最后,正如您所见,TLog.Destroy中没有CheckMaxSize的检查。因此,问题会发生,因为此类创建的文件的名称始终相同,因为它正在处理相同的任务,并且当一个对象创建该文件而另一个尝试创建另一个具有相同名称的文件时,在同一个文件夹中,操作系统(Windows)出现了异常。

<强>解决方案:

将析构函数重写为:

destructor TLog.Destroy;
begin      
  if CheckMaxSize then
    Flush;
  if FFileHandle >= 0 then
    CloseHandle(FFileHandle);
  inherited;
end;

答案 4 :(得分:-1)

如果您有需要写入单个文件的多线程代码,最好尽可能多地控制在您的手中。这意味着,避免那些你不能100%确定它们如何工作的课程。

我建议您使用多个线程&gt;单一记录器体系结构,其中每个线程将引用logger对象,并向其添加字符串。一旦达到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.