I am trying to handle a few thousand users through Indy TCP servers but i always saw very high memory consumption even with a few hundred users... i just wrote a bot to test out the performance of the server handling the data. I connected 300 bots to the test server and started sending packets through. The memory usage climbed to a few hundred MB in matter of minutes...
After going through codes i noticed that the main issue was with using sender queue for each thread so each thread can transmit its messages in its Execute function. If 300 users are sending packets to each other and writing data to each thread's queue then it cause the memory to overload... Here is what i am doing and can anyone suggest any better way to handle this?
When a client is sending message to another client this function is called and is supplied with the context of that thread/client/conneciton
Procedure TMainFrm.SendRoomBuffer(Packet: Pointer; Size: Integer; Context: TIdContext);
Var
LocalBuffer: Pointer;
Connected: Boolean;
Begin
If Size < 1 then
Exit;
Try
If Context <> Nil Then
Connected := TRoomContext(Context).Connection.Connected
Else
Connected := False;
Except
Connected := False;
End;
If Connected = True Then Begin
GetMem(LocalBuffer,Size);
CopyMemory(LocalBuffer,Packet,Size);
TRoomContext(Context).Queue.Add(LocalBuffer);
End;
End;
Iterates through all the users present in the room and send them the packet
Lst := Room.UsersList.LockList;
Try
For I := 0 To Lst.Count -1 Do Begin
Try
Username := TRoomUserInfo(Lst.Items[I]).UserName.Value;
If IncludingMe = False Then Begin
If LowerCase(Username) <> LowerCase(MyNick) Then
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[I]).Context)
End Else
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[I]).Context);
Finally
Username := '';
End;
End;
Finally
Room.UsersList.UnlockList;
Lst := Nil;
End;
This is where the actual sending is done, in the Execute of IdTCPServer
If Not TRoomContext(AContext).Queue.IsEmpty Then Begin
tmpQueue := TRoomContext(AContext).Queue.LockList;
Try
While tmpQueue.Count > 0 Do Begin
outBuffer := tmpQueue.items[0];
Try
outLen := PCommunicatorPacket(outBuffer).BufferSize;
SetLength(outBuf,outLen);
Try
CopyMemory(#outBuf[0],outBuffer,outLen);
Try
If Connected Then
AContext.Connection.IOHandler.Write(outBuf)
Finally
tmpQueue.Delete(0);
End;
Finally
SetLength(outBuf,0);
outBuf := Nil;
End;
Finally
If outBuffer <> Nil Then Begin
FreeMem(outBuffer);
outBuffer := Nil;
End;
End;
End;
Finally
TRoomContext(AContext).Queue.UnlockList;
tmpQueue := Nil;
End;
End;
Complete OnExecute Function
Procedure TMainFrm.RoomSckExecute(AContext: TIdContext);
Var Buf: TIdBytes;
Len: Integer;
outBuffer: PIdBytes;
tmpQueue: TList;
Begin
AContext.Connection.IOHandler.CheckForDataOnSource(10);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
Len := AContext.Connection.IOHandler.InputBuffer.Size;
AContext.Connection.IOHandler.ReadBytes(Buf, Len, False);
TRoomContext(AContext).ProcessPacket(#Buf[0], Len, AContext);
SetLength(Buf, 0);
Buf := nil;
end;
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
while tmpQueue.Count > 0 do begin
outBuffer := PIdBytes(tmpQueue.Items[0]);
try
tmpQueue.Delete(0);
AContext.Connection.IOHandler.Write(outBuffer^);
finally
Dispose(outBuffer);
end;
end;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
End;
If a single client is sending to 300 clients present in the room then 300 copies of the packets are made and are freed only when the actual sending is done...
If i do the writing directly to each connection and not by using queues then the memory consumption is not as rogue as this method but server hangs after a few minutes
Sorry if i forgot to mention any more details.
P.S: I am using Delphi 7
EDIT: I just check, if i dont actually write to socket, and go with the whole process as is, then the issue doesn't happen... so it means the time it takes to write to the socket, there are over a few hundred more packets read...
EDIT 2 I copied your code for the OnExecute, if i don't prove a length to ReadBytes then it takes some time about 3-5 seconds to process each command, so i am providing it with the length to read... And i used madexcept it doesnt show any leaks, i am gonna try with FastMM too now... but if there was actually a leak and something was causing it then why would commenting out the actual Write command in OnExecute suppress the memory usage?
EDIT 3 To explain my question further, i am actually reading the bytes from the stream and then process them myself later to make distinct packets from them, here is the code of what happens further after the data is read from the socket.
...
FPacketBuffer: Pointer; // global memory upto 65kb for each client to store the incoming data
PacketBufferPtr: Integer; // the offset upto where the data is read from the global memory
...
procedure TRoomContext.ProcessPacket(Buffer: Pointer; BufSize: Integer; Context: TIdContext);
begin
AddToPacketBuffer(Buffer,BufSize);
CheckAndProcessPacket(Context);
end;
procedure TRoomContext.AddToPacketBuffer(Buffer: Pointer; Size: Integer);
var
DestPtr: Pointer;
begin
if PacketBufferPtr + Size<65536 then
begin
DestPtr := Pointer(Cardinal(FPacketBuffer)+Cardinal(PacketBufferPtr));
Move(Buffer^,DestPtr^,Size);
PacketBufferPtr := PacketBufferPtr + Size;
end
else
begin
end;
end;
procedure TRoomContext.CheckAndProcessPacket(Context: TIdContext);
var DestPtr: Pointer;
NewPacketBufferLen: Integer;
SharedBuff: Pointer;
begin
if PCommunicatorPacket(FPacketBuffer).Signature = PACKET_SIGNATURE then
begin
while PCommunicatorPacket(FPacketBuffer).BufferSize <= PacketBufferPtr do
begin
GetMem(SharedBuff,PCommunicatorPacket(FPacketBuffer).BufferSize);
Try
CopyMemory(SharedBuff,FPacketBuffer,PCommunicatorPacket(FPacketBuffer).BufferSize);
MainFrm.ExecuteRoomPacket(SharedBuff, Context);
Finally
If SharedBuff <> Nil Then FreeMem(SharedBuff);
End;
NewPacketBufferLen := PacketBufferPtr - PCommunicatorPacket(FPacketBuffer).BufferSize;
DestPtr := Pointer(Cardinal(FPacketBuffer)+PCommunicatorPacket(FPacketBuffer).BufferSize);
Move(DestPtr^, FPacketBuffer^, NewPacketBufferLen);
PacketBufferPtr := NewPacketBufferLen;
end;
end
else
begin
DropInvalidPacket;
Inc(InvalidPackets);
If InvalidPackets > 50 Then
Context.Connection.Disconnect;
Exit;
end;
end;
Apologies for thinking it was because of the writing, the writing actually just slowed deletion from queue which made me think so, if i even put a sleep of 10 milliseconds, the memory consumption go rogue. About the leaks... one other reason i think this is not a leak is because if i stop the bots from messaging further, then the used memory gets back to where it was, but if i leave it running for a few minutes then it goes to a point where the application hangs or i receive an out of memory message. I think the issue is with making copies, i tried using a global queue for the room to handle messages and so multiple copies aren't made of the data, but that cause the application to hang after sometime maybe too much thread contention or i am not playing it safe.
TCP does not support broadcasting, and directly writing to TIdTCPServer connections from outside the server's events is generally not thread-safe (although it can be done if you are careful). In your situation, using queues is a good idea.
However, don't call Connected() in SendRoomBuffer(). It performs a read operation, which can interfere with any reading the OnExecute event handler performs, and can corrupt the InputBuffer's content by reading socket data out of order. If Context is not nil then queue the data regardless of the socket state, and catch any errors.
Also, in the OnDisconnect event, make sure you are freeing any queued packets that were not sent, otherwise you will leak them.
Lastly, your OnExecute code is making another copy of the queued data and then sending that copy (I am assuming that outBuf is a TIdBytes). Try to avoid that. I would suggest you change your queue to store TMemoryStream or TIdBytes objects instead of raw memory blocks, then you can pass the queued items directly to IOHandler.Write() without having to make copies of them first.
Try something like this:
type
PIdBytes = ^TIdBytes;
procedure TMainFrm.SendRoomBuffer(Packet: Pointer; Size: Integer; Context: TIdContext);
var
LocalBuffer: PIdBytes;
begin
if (Packet = nil) or (Size < 1) or (Content = nil) then
Exit;
New(LocalBuffer);
try
LocalBuffer^ := RawToBytes(Packet^, Size);
TRoomContext(Context).Queue.Add(LocalBuffer);
except
Dispose(LocalBuffer);
end;
end;
Lst := Room.UsersList.LockList;
try
for i := 0 To Lst.Count -1 do begin
Username := TRoomUserInfo(Lst.Items[i]).UserName.Value;
if (not IncludingMe) and TextIsSame(Username, MyNick) then begin
Continue;
end;
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[i]).Context);
end;
finally
Room.UsersList.UnlockList;
end;
procedure TMainFrm.RoomSckDisconnect(AContext: TIdContext);
var
tmpQueue: TList;
i: Integer;
begin
...
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
for i := 0 to tmpQueue.Count-1 do begin
Dispose(PIdBytes(tmpQueue.Items[i]));
end;
tmpQueue.Clear;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
...
end;
procedure TMainFrm.RoomSckExecute(AContext: TIdContext);
var
Buf: TIdBytes;
outBuffer: PIdBytes;
tmpQueue: TList;
begin
AContext.Connection.IOHandler.CheckForDataOnSource(10);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
AContext.Connection.IOHandler.ReadBytes(Buf, -1, False);
TRoomContext(AContext).ProcessPacket(#Buf[0], Len, AContext);
SetLength(Buf, 0);
Buf := nil;
end;
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
while tmpQueue.Count > 0 do begin
outBuffer := PIdBytes(tmpQueue.Items[0]);
try
tmpQueue.Delete(0);
AContext.Connection.IOHandler.Write(outBuffer^);
finally
Dispose(outBuffer);
end;
end;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
end;
Related
I have one multi-thread application that needs to post data via idhttp, to some http hosts... The number of the hosts varies and I put them inside one TXT file that is read into a TStringList. But it's something around 5k hosts daily. Ok, after 3 days running, more or less, and around 15k hosts checked, the threads start hanging at some point of the code, and the program becomes very slow, like it start checking 1 host per 10 minutes... Sometimes it goes far, and stay 1 week running very nicely, but after this same problem: looks like most of the threads start hanging... I don't know where exactly is the problem, because I run it with 100 threads, and like I said, after 15k or more hosts it start becoming slow...
Here's the almost entire source code (sorry to posting entire, but I think it's better more than less)
type
MyThread = class(TThread)
strict private
URL, FormPostData1, FormPostData2: String;
iData1, iData2: integer;
procedure TerminateProc(Sender: TObject);
procedure AddPosted;
procedure AddStatus;
function PickAData: bool;
function CheckHost: bool;
function DoPostData(const FormPostData1: string; const FormPostData2: string): bool;
protected
constructor Create(const HostLine: string);
procedure Execute; override;
end;
var
Form1: TForm1;
HostsFile, Data1, Data2: TStringList;
iHost, iThreads, iPanels: integer;
MyCritical: TCriticalSection;
implementation
function MyThread.CheckHost: bool;
var
http: TIdHTTP;
code: string;
begin
Result:= false;
http:= TIdHTTP.Create(Nil);
http.IOHandler:= TIdSSLIOHandlerSocketOpenSSL.Create(http);
http.Request.UserAgent:= 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
http.HandleRedirects:= True;
try
try
code:= http.Get(URL);
if(POS('T2ServersForm', code) <> 0) then
Result:= true;
except
Result:= false;
end;
finally
http.Free;
end;
end;
function MyThread.PickAData: bool;
begin
Result:= false;
if (iData2 = Data2.Count) then
begin
inc(iData1);
iData2:= 0;
end;
if iData1 < Data1.Count then
begin
if iData2 < Data2.Count then
begin
FormPostData2:= Data2.Strings[iData2];
inc(iData2);
end;
FormPostData1:= Data1.Strings[iData1];
Result:= true;
end;
end;
function MyThread.DoPostData(const FormPostData1: string; const FormPostData2: string): bool;
var
http: TIdHTTP;
params: TStringList;
response: string;
begin
Result:= false;
http:= TIdHTTP.Create(Nil);
http.Request.UserAgent := 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
http.Request.ContentType := 'application/x-www-form-urlencoded';
params:= TStringList.Create;
try
params.Add('LoginType=Explicit');
params.Add('Medium='+FormPostData1);
params.Add('High='+FormPostData2);
try
response:= http.Post(Copy(URL, 1, POS('?', URL) - 1), params);
if http.ResponseCode = 200 then
Result:= true;
except
if (http.ResponseCode = 302) then
begin
if(POS('Invalid', http.Response.RawHeaders.Values['Location']) = 0) then
Result:= true;
end
else
Result:= true;
end;
finally
http.Free;
params.Free;
end;
end;
procedure MyThread.AddPosted;
begin
Form1.Memo1.Lines.Add('POSTED: ' + URL + ':' + FormPostData1 + ':' + FormPostData2)
end;
procedure MyThread.AddStatus;
begin
inc(iPanels);
Form1.StatusBar1.Panels[1].Text:= 'Hosts Panels: ' + IntToStr(iPanels);
end;
procedure MainControl;
var
HostLine: string;
begin
try
MyCritical.Acquire;
dec(iThreads);
while(iHost <= HostsFile.Count - 1) and (iThreads < 100) do
begin
HostLine:= HostsFile.Strings[iHost];
inc(iThreads);
inc(iHost);
MyThread.Create(HostLine);
end;
Form1.StatusBar1.Panels[0].Text:= 'Hosts Checked: ' + IntToStr(iHost);
if(iHost = HostsFile.Count - 1) then
begin
Form1.Memo1.Lines.Add(#13#10'--------------------------------------------');
Form1.Memo1.Lines.Add('Finished!!');
end;
finally
MyCritical.Release;
end;
end;
{$R *.dfm}
constructor MyThread.Create(const HostLine: string);
begin
inherited Create(false);
OnTerminate:= TerminateProc;
URL:= 'http://' + HostLine + '/ServLan/Controller.php?action=WAIT_FOR';
iData2:= 0;
iData1:= 0;
end;
procedure MyThread.Execute;
begin
if(CheckHost = true) then
begin
Synchronize(AddStatus);
while not Terminated and PickAData do
begin
try
if(DoPostData(FormPostData1, FormPostData2) = true) then
begin
iData1:= Data1.Count;
Synchronize(AddPosted);
end;
except
Terminate;
end;
end;
Terminate;
end;
end;
procedure MyThread.TerminateProc(Sender: TObject);
begin
MainControl;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
if (FileExists('data2.txt') = false) OR (FileExists('data1.txt') = false) then
begin
Button1.Enabled:= false;
Memo1.Lines.Add('data2.txt / data1.txt not found!!');
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
openDialog : TOpenDialog;
begin
try
HostsFile:= TStringList.Create;
openDialog := TOpenDialog.Create(Nil);
openDialog.InitialDir := GetCurrentDir;
openDialog.Options := [ofFileMustExist];
openDialog.Filter := 'Text File|*.txt';
if openDialog.Execute then
begin
HostsFile.LoadFromFile(openDialog.FileName);
Button2.Enabled:= true;
Button1.Enabled:= false;
end;
finally
openDialog.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Button2.Enabled:= false;
Data1:= TStringList.Create;
Data1.LoadFromFile('data1.txt');
Data2:= TStringList.Create;
Data2.LoadFromFile('data2.txt');
MyCritical:= TCriticalSection.Create;
iHost:= 0;
iThreads:= 0;
MainControl;
end;
You are constantly creating threads without freeing them. This means that your system will grow out of resources (windows handles, or memory) after a while.
Set FreeOnTerminate := true in the thread constructor to free the thread when terminated.
If you declared ReportMemoryLeaksOnShutdown := true when you started the program in debug mode, this leak would have been reported.
MainControl is called from the main thread only and data used there are not accessed from other threads, so there is no need for a critical section.
Using a thread pool will also help to make the application more responsive.
IMO, your thread is getting trapped inside your MyThread.Execute while loop. There is no guarantee that once inside that loop it will exit (because the DoPostData() method depends on some external response). This way, I bet that, one by one, each thread is getting stuck in there until few (or none) remain working.
You should add some log capabilities to your MyThread.Execute() just to be sure that it is not dying somewhere... You can also add a fail safe exit condition there (e.g. if (TriesCount > one zillion times) then exit).
Also, I consider a better design to keep your threads running all the time and just provide new work to them, instead of creating/destroying the threads, i.e. create your 100 threads in the beginning and only destroy them at the end of your program execution. But it requires significant changes to your code.
First, I would trap & log exceptions.
Second, this appears to infinitely build Form1.Memo1. What happens when you run the system out of memory this way? Or exceed it's capacity. (It's been long enough since I've dealt with Delphi, I don't recall if there's a limit in this regard or not. There certainly is if this is 32 bit code.)
Just at a first glance, I'd recommend adding the http := TIdHTTP(Nil) to the TThread.Create event and the http.Free to the Destroy event for TThread. Not sure if that will solve the issue. Windows does have a OS limit on threads per process (can't remember well but the number 63 comes to mind. You may want to create a thread pool to cache your thread requests. It might perform more reliabily with a "thundering herd" of requests. I'm suspecting at that number of requests some of the threads may be terminating abnormally which could slow things down, leak memory, etc. Enabling FullDebugMode and LogMemoryLeakDetailsToFile to check for leaks might reveal something. Checking the task manager to watch the memory used by the running process is another luke warm indicator of a problem; memory usage grows and never releases.
Best of luck.
RP
What is the proper way to create a thread that will read received data with IdTCPClient1 ? and all data are in TStream or Tmemorystream form.
I tried:
constructor TReadResponse.Create(AConn: TIdTCPConnection);
begin
FConn := AConn;
inherited Create(False);
end;
procedure TReadResponse.Execute;
var
RcvStrMem : TMemoryStream;
begin
while not Terminated and FConn.Connected do
begin
try
RcvStrMem := TMemoryStream.Create;
FConn.IOHandler.LargeStream := True;
FConn.IOHandler.ReadStream(RcvStrMem, -1, False);//error here ??
//MessageBox(0, pChar(inttostr(RcvStrMem.Size)), 0, 0);
TWriteResponse.AddResponse(RcvStrMem);
finally
RcvStrMem.Free;
end;
end;
end;
WriteResponse thread
class procedure TWriteResponse.AddResponse (AResponse: TStream);
begin
with Create(AResponse) do try
Synchronize;
finally
Free;
end;
end;
procedure TWriteResponse.DoSynchronize;
begin
//do something
end;
Here's how I execute the thread in Form1:
var
rr: TReadResponse = nil;
......
procedure TForm1.IdTCPClient1Connected(Sender: TObject);
begin
rr:= TReadResponse.Create(IdTCPClient1);
end;
The code you showed is OK, provided that every TCP message is preceded by an Int64, in network byte order, specifying the size of the message, as that is what you are telling ReadStream() to read by setting LargeStream=True, AByteCount=-1, and AReadUntilDisconnect=False. It will read 8 bytes and interpret them as an Int64, and then read however many bytes the Int64 says.
Since you did not provide any details about your actual TCP protocol, noone can tell you whether you are reading the TCP messages correctly or not. You said there is an error on the ReadStream(), but you did not say what the error actually is. But the fact that you are getting an error suggests the TCP messages are NOT in the format that ReadStream() is expecting.
If you want more help, you need to show what the TCP messages actually look like.
I want to read blobfield (with blobstream) from client side (over network) but application freezes while fetching data. How can I read blobfield without freezing and showing percentage with a progressbar. (I'm using Delphi and Firebird)
i'm using uniquery component. i've found this code from: http://forums.devart.com/viewtopic.php?t=14629
but it doesn't work properly:
const
BlockSize= $F000;
var
Blob: TBlob;
Buffer: array of byte;
p: pointer;
pos, count: integer;
UniQuery1.SQL.Text:= 'select * from TABLE1 where FIELD_ID = 1';
UniQuery1.Open;
blob:= uniquery1.GetBlob('DATA');
SetLength(buffer, blob.Size);
ProgressBar1.Position:= 0;
Application.ProcessMessages;
repeat
count:= Blob.Read(pos, blocksize, p);
ProgressBar1.Position:= Round(pos/Blob.Size * 100);
pos:= pos + count;
p:= pointer(integer(p) + count);
Application.ProcessMessages;
until count < blocksize;
PS: i've set uniquery's options:
cacheblobs:= false;
streamedblobls:= true;
deferredblobread:= true;
in the first step of repeat-until loop, Blob.Read method reads all of stream, so it doesnt work properly.
You should use a thread, here is an example with Delphi TThread:
type
TMyForm = class(TForm)
private
FPosition: Integer;
procedure ProgressUpdate;
procedure Execute;
end;
procedure TMyForm.ProgressUpdate;
begin
ProgressBar1.Position := FPosition;
end;
procedure TMyForm.Execute;
begin
FPosition:= 0;
ProgressUpdate;
Thread := TThread.CreateAnonymousThread(procedure
begin
repeat
// Do some long running stuff (in chunks, so we can update the position)
FPosition := CalculatePosition;
// Important: Synchronize will run ProgressUpdate in the main thread!
TThread.Synchronize(nil, ProgressUpdate);
until SomeCondition;
end
);
Thread.Start;
end;
So after applying this pattern to your code we get:
type
TMyForm = class(TForm)
private
FPosition: Integer;
procedure ProgressUpdate;
procedure Execute;
end;
procedure TMyForm.ProgressUpdate;
begin
ProgressBar1.Position := FPosition;
end;
procedure TMyForm.Execute;
var
Blob: TBlob;
Thread: TThread;
begin
UniQuery1.SQL.Text := 'SELECT * FROM TABLE1 WHERE FIELD_ID = 1';
UniQuery1.Open;
Blob := UniQuery1.GetBlob('DATA');
FPosition:= 0;
ProgressUpdate;
Thread := TThread.CreateAnonymousThread(
procedure
const
BlockSize = $F000;
var
Buffer: array of Byte;
P: Pointer;
Pos, Count: Integer;
begin
SetLength(Buffer, Blob.Size);
repeat
Count := Blob.Read(Pos, BlockSize, P);
FPosition := Round(Pos / Blob.Size * 100);
Pos := Pos + Count;
P := Pointer(Integer(P) + Count);
// Important: Synchronize will run ProgressUpdate in the main thread!
TThread.Synchronize(nil, ProgressUpdate);
until Count < BlockSize;
end
);
Thread.Start;
end;
I removed the Application.ProcessMessage and moved all processing to the thread.
The Thread is setting the FPosition private attribute and uses TThread.Synchronize to set the ProgressBar position to FPosition in the main thread.
If your block size is not big enough this might still block the UI (due to excessive synchronization), so choose an appropriate block size or add some update delay.
You have to make sure that the connection of the UniQuery1 object is not used in the main thread while the anonymous thread is running or move the connection and query to the thread as well.
Also this can have reentrance problems, but it should give you a basic idea of how to use a thread for background processing.
PS: It might also be a good idea to run the query in the thread, especially if it can take some time.
In my Delphi project, I derive a thread class TMyThread, and follow the advice from forums to use AllocateHWnd to create a window handle. In TMyThread object, I call SendMessage to send message to the window handle.
When the messages sent are in small volume, then the application works well. However, when the messages are in large volume, the application will deadlock and lose responses. I think may be the message queue is full as in LogWndProc, there are only codes to process the message, but no codes to remove the messages from the queue, that may cause all the processed messages still exist in the queue and the queue becomes full. Is that correct?
The codes are attached below:
var
hLogWnd: HWND = 0;
procedure TForm1.FormCreate(Sender: TObject);
begin
hLogWnd := AllocateHWnd(LogWndProc);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if hLogWnd <> 0 then
DeallocateHWnd(hLogWnd);
end;
procedure TForm1.LogWndProc(var Message: TMessage);
var
S: PString;
begin
if Message.Msg = WM_UPDATEDATA then
begin
S := PString(msg.LParam);
try
List1.Items.Add(S^);
finally
Dispose(S);
end;
end else
Message.Result := DefWindowProc(hLogWnd, Message.Msg, Message.WParam,
Message.LParam);
end;
procedure TMyThread.SendLog(I: Integer);
var
Log: PString;
begin
New(Log);
Log^ := 'Log: current stag is ' + IntToStr(I);
SendMessage(hLogWnd, WM_UPDATEDATA, 0, LPARAM(Log));
Dispose(Log);
end;
You are disposing your allocated string twice. At best, you will get an exception in your worker thread after SendMessage() has exited, terminating your thread if you do not catch that exception. At worse, you might not get an exception, but you will trash memory, leaving your app in a bad state so all sorts of random things can happen. You need to dispose the allocated string only once.
You are not responsible for removing sent messages from the queue because SendMessage() does not put the message into the queue. However, it does require the receiving thread to pump its queue for new messages, even if there are no new messages in the queue, in order to dispatch sent messages that are crossing thread boundaries, like your message is. If SendMessage() is blocking then your main thread is not pumping the queue correctly in code you have not shown, such as if you have other code that has blocked the main message loop from running.
As for the code you did show, I would suggest the following change:
procedure TForm1.LogWndProc(var Message: TMessage);
begin
if Message.Msg = WM_UPDATEDATA then
List1.Items.Add(PString(Message.LParam)^)
else
Message.Result := DefWindowProc(hLogWnd, Message.Msg, Message.WParam, Message.LParam);
end;
procedure TMyThread.SendLog(I: Integer);
var
Log: String;
begin
Log := 'Log: current stag is ' + IntToStr(I);
SendMessage(hLogWnd, WM_UPDATEDATA, 0, LPARAM(#Log));
end;
You do not need to dynamically allocate the string if you use SendMessage(), since it blocks the calling thread until the message is processed, ensuring the string remains valid. If you were using PostMessage() instead, then you would need to dynamically allocate (and fix your erroneous use of Dispose()):
procedure TForm1.LogWndProc(var Message: TMessage);
var
S: PString;
begin
if Message.Msg = WM_UPDATEDATA then
begin
S := PString(msg.LParam);
try
List1.Items.Add(S^);
finally
Dispose(S);
end;
end else
Message.Result := DefWindowProc(hLogWnd, Message.Msg, Message.WParam, Message.LParam);
end;
procedure TMyThread.SendLog(I: Integer);
var
Log: PString;
begin
New(Log);
Log^ := 'Log: current stag is ' + IntToStr(I);
if not PostMessage(hLogWnd, WM_UPDATEDATA, 0, LPARAM(Log)) then
Dispose(Log);
end;
In my Application when I write text files (logs, traces, etc), I use TFileStream class.
There are cases that I write the data in multithreaded environment, those are the steps:
1- Write Cache Data
2- For each 1000 lines I save to File.
3- Clear Data.
This process is repeated during all processing.
Problem Description:
With 16 threads, the system throws the following exception:
Access Violation - file already in use by another application.
I guess this is happening because that the handle used by one thread is not closed yet, when another thread needs to open.
I changed the architecture to the following: (bellow is the NEW implementation)
In the previous way, the TFileStream was created with FileName and Mode parameters, and destroyed closing the handle (I wasn't using 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;
There is another better way?
Is this implementation correct?
May I improve this?
My guess about the Handle was right?
All theads use the same Log object.
There is no reentrance, i checked! there is something wrong with the TFileStream.
The Access to the Add is synchronized, I mean, I used critical session, and when it reaches 1000 lines, Flush procedure is called.
P.S: I do not want third-party component, i want to create my own.
Well, for a start, there's no point in TMyFileStream. What you are looking for is THandleStream. That class allows you to supply a file handle whose lifetime you control. And if you use THandleStream you'll be able to avoid the rather nasty hacks of your variant. That said, why are you even bothering with a stream? Replace the code that creates and uses the stream with a call to SetFilePointer to seek to the end of the file, and a call to WriteFile to write content.
However, even using that, your proposed solution requires further synchronization. A single windows file handle cannot be used concurrently from multiple threads without synchronisation. You hint in a comment (should be in the question) that you are serializing file writes. If so then you are just fine.
The threaded solution provided by Marko Paunovic quite nice, however while reviewing the code I noticed a small mistake, perhaps just an oversight in the example but I thought I'd mention it just the same in case someone actually tries to use it as-is.
There is a missing call to Flush in TLogger.Destroy, as a result any unflushed (buffered) data is disgarded when the TLogger object is destroyed.
destructor TLogger.Destroy;
begin
if FStrings.Count > 0 then
Flush;
FStrings.Free;
DeleteCriticalSection(FLock);
inherited;
end;
How about:
In each thread, add log lines to a TStringList instance until lines.count=1000. Then push the TStringList onto a blocking producer-consumer queue, immediately create a new TStringList and carry on logging to the new list.
Use one Logging thread that dequeues the TStringList instances, writes them to the file and then frees them.
This isolates the log writes from disk/network delays, removes any reliance on dodgy file-locking and will actually work reliably.
I figured MY MISTAKE.
In first place, I want to apologize for posting this stupid question without a proper way to reproduce the exception. In other words, without a SSCCE.
The problem was a control flag that my TLog class used internally.
This flag was created, when we started to evolve our product a parallel architecture.
As we needed to keep the previous form working (at least until everything was in the new architecture).
We created some flags to identify if the object was either the new or old version.
One of that flags was named CheckMaxSize.
If CheckMaxSize was enabled, at a certain moment, every data inside the instance of this object in each thread, would be thrown to the main instance, which was in the "main" thread (not the GUI one, because it was a background work). Furthermore, when CheckMaxSize is enabled, TLog should never ever call "flush".
Finally, as you can see, in TLog.Destroy there is no check to CheckMaxSize. Therefore, the problem would happen because the name of the file created by this class was always the same, since it was processing the same task, and when One object created the file and another one tried to create another file with the same name, inside the same folder, the OS (Windows) rose an Exception.
Solution:
Rewrite the destructor to:
destructor TLog.Destroy;
begin
if CheckMaxSize then
Flush;
if FFileHandle >= 0 then
CloseHandle(FFileHandle);
inherited;
end;
If you have multithreaded code that needs to write to single file, it's best to have as much control as you can in your hands. And that means, avoid classes which you are not 100% sure how they work.
I suggest that you use multiple threads > single logger architecture, where each thread will have reference to logger object, and add strings to it. Once 1000 lines are reached, logger would flush the collected data in file.
There is no need to use TFileStream to write data to file, you can
go with CreateFile()/SetFilePointer()/WriteFile(), as David already suggested
TStringList is not thread-safe, so you have to use locks on it
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.