在我的应用程序中,当我写文本文件(日志,跟踪等)时,我使用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:我不想要第三方组件,我想创建自己的组件。
答案 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行,记录器将刷新文件中收集的数据。
<强> 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.