高吞吐量的一个日志函数类,Delphi

在开发服务器端程序的时候,日志是必须的一个功能。由于服务器端的要频繁的把数据写入日志,开始的时候用了一个很简单日志函数

就是直接把日志字符写入文件中。然后关闭连接。一直也应用良好。但做压力测试的时候,因为要每个连接的数据都要写入日志,发现运行的一段时间后,频繁掉线,CPU占用率,居高不下,优化了可以想到的很多地方,有一定的效果,仔细观察发现,硬盘灯狂闪不止,说明硬盘I/0操作过于紧张。但测试的时候,基本是不读写硬盘的,恍然发现,是日志函数影响到整个系统的性能。每一个日志数据的时候,就要打开文件,写入文件,关闭文件。哈,这些都是相对昂贵的I/0操作。优化的方法很简单,缓存数据,定期的批量写入磁盘。基于此设计思路就开发了一个新的日志类。以空间换取时间。

内部采用双缓冲算法,写入信息的时候,是直接写入到 内存中,然后线程根据一定的时间间隔,将内存中的数据写到磁盘文件中,里面开辟了两块缓冲内存队列,采用了生产者===》消费者模式,WriteLog 是写入日志数据,算是数据的生产者,TFileStream对象,将内存中的数据写入磁盘是消费者角色,由于采用了双缓冲方式,减少了生产与消费间的干扰. 提高了性能,减少日志的写入时间。也勉强算是个双缓冲队列的实际应用.

此日志类是基于线程实现的。为了方便使用。内部采用了锁定机制。是线程安全的类。当数据量比较大或者为了便于日志文件的管理

我们会把数据按一定规则生成不同的日志文件名,最常见的就是按日期作日志文件的名称。例如 20110702.log, 20110701.log 等

此日志类中考虑到此情况,可以随时更改日志文件名。

property FileName:string read getLogFileName write setLogFileName; 要修改日志文件名,直接赋值即可。也是线程安全的。

最后要说明下,此日志类的设计思路也可以用于其它方面。缓冲,空间换时间是软件设计中常用的方法。

//实现的代码

unit uSfLog;

interface

uses

Windows, Messages, SysUtils, Variants, Classes;

type

TsfLog=class(TThread)

private

FLF:string;//#13#10;

FS:TFileStream;

FCurFileName:string;

FFileName:string;

FBegCount:DWord;

FBuffA,FBuffB:TMemoryStream;

FCS:TRTLCriticalSection;

FCS_FileName:TRTLCriticalSection;

FLogBuff:TMemoryStream;

procedure WriteToFile();

function getLogFileName: string;

procedure setLogFileName(const Value: string);

protected

procedure Execute();override;

public

constructor Create(LogFileName:string);

destructor Destroy();override;

procedure WriteLog(const InBuff:Pointer;InSize:Integer);overload;

procedure WriteLog(const Msg:string);overload;

public

property FileName:string read getLogFileName write setLogFileName;

end;

implementation

{ TsfLog }

constructor TsfLog.Create(LogFileName:string);

begin

if Trim(LogFileName) = '' then

raise exception.Create('Log FileName not ""');

inherited Create(TRUE);

//\\

InitializeCriticalSection(FCS); //初始化

InitializeCriticalSection(FCS_FileName);//日志文件名

//队列缓冲区A,B运行的时候,交替使用

Self.FBuffA := TMemoryStream.Create();

Self.FBuffA.Size := 1024 * 1024; //初始值可以根据需要自行调整

Self.FBuffB := TMemoryStream.Create();

Self.FBuffB.Size := 1024 * 1024; //初始值可以根据需要自行调整

Self.FLogBuff := Self.FBuffA;

if FileExists(LogfileName) then

begin

FS := TFileStream.Create(LogFileName,fmOpenWrite or fmShareDenyWrite);

FS.Position := FS.Size; //如果文件已经存在,数据进行追加

end

else

FS := TFileStream.Create(LogFileName,fmCreate or fmShareDenyWrite);

FCurFileName := LogFileName;

FFileName := LogFileName;

FLF := #13#10;

//启动执行

Self.Resume();

//\\

end;

destructor TsfLog.Destroy;

begin

FBuffA.Free();

FBuffB.Free();

FS.Free();

inherited;

end;

procedure TsfLog.Execute();

begin

FBegCount := GetTickCount();

while(not Self.Terminated) do

begin

//2000ms 可以根据自己的需要调整,数据写入磁盘的间隔

if (GetTickCount() - FBegCount) >= 2000 then

begin

WriteToFile();

FBegCount := GetTickCount();

end

else

Sleep(200);

end;

WriteToFile();

end;

function TsfLog.getLogFileName: string;

begin

EnterCriticalSection(FCS_FileName);

try

Result := FCurFileName;

finally

LeaveCriticalSection(FCS_FileName);

end;

end;

procedure TsfLog.setLogFileName(const Value: string);

begin

EnterCriticalSection(FCS_FileName);

try

FFileName := Value;

finally

LeaveCriticalSection(FCS_FileName);

end;

end;

procedure TsfLog.WriteLog(const Msg: string);

begin

WriteLog(Pointer(Msg),Length(Msg));

end;

procedure TsfLog.WriteLog(const InBuff: Pointer; InSize: Integer);

var

TmpStr:string;

begin

TmpStr := FormatDateTime('YYYY-MM-DD hh:mm:ss zzz ',Now());

EnterCriticalSection(FCS);

try

FLogBuff.Write(TmpStr[1],Length(TmpStr));

FLogBuff.Write(InBuff^,InSize);

FLogBuff.Write(FLF[1],2);

finally

LeaveCriticalSection(FCS);

end;

end;

procedure TsfLog.WriteToFile;

var

MS:TMemoryStream;

IsLogFileNameChanged:Boolean;

begin

EnterCriticalSection(FCS);

//交换缓冲区

try

MS := nil;

if FLogBuff.Position > 0 then

begin

MS := FLogBuff;

if FLogBuff = FBuffA then FLogBuff := FBuffB

else

FLogBuff := FBuffA;

FLogBuff.Position := 0;

end;

finally

LeaveCriticalSection(FCS);

end;

//\\

if MS = nil then

Exit;

//写入文件

try

FS.Write(MS.Memory^,MS.Position);

finally

MS.Position := 0;

end;

//检测文件名称是否变化

EnterCriticalSection(FCS_FileName);

try

IsLogFileNameChanged := (FCurFileName <> FFileName);

finally

LeaveCriticalSection(FCS_FileName);

end;

//日志文件名称修改了

if IsLogFileNameChanged then

begin

FCurFileName := FFileName;

FS.Free();

if FileExists(FFileName) then

begin

FS := TFileStream.Create(FFileName,fmOpenWrite or fmShareDenyWrite);

FS.Position := FS.Size;

end

else

FS := TFileStream.Create(FFileName,fmCreate or fmShareDenyWrite);

end;

end;

end.

//日志类函数的测试代码

//主要测试三个功能

1)日志的写入速度是否足够快

2)日志类在多线程情况下,能稳定运行吗?

3)运行中,更换输出日志的文件名称

为了产生大量的数据及多线程下的稳定性,测试中产生了 120个线程,同时写日志函数。

同时用一个定时器,定期的修改输出日志的文件名。写入日志的信息是随机产生的GUID字符串。

unit uMain;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs,uSfLog, StdCtrls, ExtCtrls,ActiveX;

type

TfrmMain = class(TForm)

Button1: TButton;

Edit1: TEdit;

Timer1: TTimer;

procedure FormCreate(Sender: TObject);

procedure Button1Click(Sender: TObject);

procedure FormClose(Sender: TObject; var Action: TCloseAction);

procedure Timer1Timer(Sender: TObject);

private

{ Private declarations }

FList:TList;

LogObj:TsfLog;

public

{ Public declarations }

end;

TsfLogTest=class(TThread)

protected

procedure Execute();override;

end;

var

frmMain: TfrmMain;

implementation

{$R *.dfm}

function GetGUID():string;

var

ID:TGUID;

begin

CoCreateGuid(ID);

Result := GUIDToString(ID);

end;

procedure TfrmMain.FormCreate(Sender: TObject);

begin

LogObj := TsfLog.Create('C:\temp\0001.TXT');

FList := TList.Create();

end;

//启动测试

procedure TfrmMain.Button1Click(Sender: TObject);

var

Obj:TsfLogTest;

Index:Integer;

begin

for Index := 1 to 120 do

begin

Obj := TsfLogTest.Create(FALSE);

FList.Add(Obj);

end;

Self.Timer1.Enabled := TRUE;

end;

{ TsfLogTest }

procedure TsfLogTest.Execute;

var

Msg:string;

begin

while(not self.Terminated) do

begin

Msg := IntToStr(Self.ThreadID) + #09 +

GetGUID() + GetGUID() + GetGUID() + GetGUID() +

GetGUID() + GetGUID() + GetGUID() + GetGUID();

frmMain.LogObj.WriteLog(Msg);

Sleep(10);

end;

end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);

var

Index:Integer;

Obj:TsfLogTest;

begin

for Index := 0 to FList.Count - 1 do

begin

Obj:= TsfLogTest(FList.Items[Index]);

Obj.Terminate();

end;

Sleep(100);

end;

procedure TfrmMain.Timer1Timer(Sender: TObject);

var

AFileName:string;

begin

AFileName := 'C:\Temp\' + FormatDateTime('YYYYMMDD_hhmmss_zzz',Now()) + '.TXT';

LogObj.FileName := AFileName;

end;

end.