-3

在我的应用程序中,当我编写文本文件(日志、跟踪等)时,我使用TFileStreamclass. 在某些情况下,我在多线程环境中编写数据,这些是步骤:

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 过程。

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

4

5 回答 5

1

好吧,首先,没有任何意义TMyFileStream。您正在寻找的是THandleStream. 该类允许您提供您控制其生命周期的文件句柄。而且,如果您使用THandleStream,您将能够避免变体的相当讨厌的黑客攻击。也就是说,你为什么还要为流而烦恼?将创建和使用流的代码替换为SetFilePointer对文件末尾的调用和 WriteFile对写入内容的调用。

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

于 2013-05-14T13:08:13.597 回答
1

Marko Paunovic 提供的线程解决方案非常好,但是在查看代码时我注意到一个小错误,可能只是示例中的一个疏忽,但我想我会提到它,以防有人实际尝试按原样使用它.

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

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

  FStrings.Free;
  DeleteCriticalSection(FLock);

  inherited;
end;
于 2013-09-01T15:52:44.010 回答
0

怎么样:

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

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

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

于 2013-05-14T13:13:26.843 回答
0

我想出了我的错误。

首先,我想为发布这个愚蠢的问题而没有正确的方法来重现异常表示歉意。换句话说,没有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;
于 2013-12-03T13:22:16.533 回答
-1

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

我建议您使用多线程 > 单记录器架构,其中每个线程都将引用记录器对象,并向其添加字符串。一旦达到 1000 行,记录器将刷新文件中收集的数据。

  • 无需使用 TFileStream 将数据写入文件,您可以使用 CreateFile()/SetFilePointer()/WriteFile(),正如 David 已经建议的那样
  • TStringList 不是线程安全的,所以你必须在它上面使用锁

主.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.
于 2013-05-14T14:18:01.907 回答