The program which is giving this error. Sometimes immediately, sometimes after a short time
http://www1.datafilehost.com/d/39f524c0 Thread Suspends in some try finally block
Source:
http://www1.datafilehost.com/d/1cae7b24 EOufOfResources During debugging
Im sorry for bad English. I have the following problem: I try to do 5 fps screenshots and draw the cursor icon on them, recode BMP in PNG and send it over the network through blocking sockets Indy. After sending a screenshot proportionally compressed and placed on TImage (desktopimage) on the main form. If I'm doing all this in a timer - that everything works fine if I am doing all of this code in Synchronize() it also works fine, but it causes freezing of the interface, I want to get rid of it, and doing so in PNG compression in the thread, now I tried to break several Synchronize() to find the error (I get an error EOutOfResources), but I could not. Please help. Here is my code:
TCaptureThread = class(TThread)
private
bmp: TBitmap;
DC: HDC;
h:hwnd;
thumbRect : TRect;
maxWidth, maxHeight:integer;
png:TPNGImage;
Stream:TMemoryStream;
RecBlock:TCommBlock;
r: TRect;
CI: TCursorInfo;
Icon: TIcon;
II: TIconInfo;
commblock:TCommblock;
procedure showthumb;
procedure send;
procedure stretch;
procedure getscreen;
procedure fixsize;
protected
procedure Execute; override;
constructor Create(CreateSuspended: Boolean);
destructor destroy; override;
end;
constructor TCaptureThread.Create(CreateSuspended: Boolean);
begin
bmp:=TBitmap.Create;
Stream:=TMemoryStream.Create;
png:=TPNGImage.Create;
Icon := TIcon.Create;
inherited Create(CreateSuspended);
end;
destructor TCaptureThread.destroy;
begin
png.Free;
bmp.Free;
Icon.Free;
stream.Free;
inherited;
end;
procedure TCaptureThread.Execute;
begin
inherited;
while not Terminated do
begin
Synchronize(fixsize);
Synchronize(getscreen);
r := bmp.Canvas.ClipRect;
try
CI.cbSize := SizeOf(CI);
if GetCursorInfo(CI) then
if CI.Flags = CURSOR_SHOWING then
begin
Icon.Handle := CopyIcon(CI.hCursor);
if GetIconInfo(Icon.Handle, II) then
begin
bmp.Canvas.Draw(
ci.ptScreenPos.x - Integer(II.xHotspot) - r.Left - Form4.Left,
ci.ptScreenPos.y - Integer(II.yHotspot) - r.Top - Form4.Top,
Icon
);
end;
end;
finally
end;
try
png.Assign(bmp);
png.CompressionLevel := 9;
png.SaveToStream(stream);
stream.Position :=0;
Recblock.Command :='STREAM';
Recblock.Msg :='';
Recblock.NameFrom := MyName;
Synchronize(send);
finally
end;
try
thumbRect.Left := 0;
thumbRect.Top := 0;
if bmp.Width > bmp.Height then
begin
thumbRect.Right := maxWidth;
thumbRect.Bottom := (maxWidth * bmp.Height) div bmp.Width;
end
else
begin
thumbRect.Bottom := maxHeight;
thumbRect.Right := (maxHeight * bmp.Width) div bmp.Height;
end;
Synchronize(stretch);
bmp.Width := thumbRect.Right;
bmp.Height := thumbRect.Bottom;
Synchronize(showthumb);
finally
end;
sleep(200);
end;
end;
procedure TCaptureThread.getscreen;
begin
DC:=GetDC(0);
bitblt(bmp.Canvas.Handle, 0, 0, Form4.Width+Form4.Left, Form4.Height+Form4.Top,
DC, Form4.Left, Form4.Top, SRCCOPY);
ReleaseDC(0, DC);
end;
procedure TCaptureThread.fixsize;
begin
maxWidth := Form1.DesktopImage.Width;
maxHeight := Form1.DesktopImage.Height;
bmp.Height:=Form4.Height;
bmp.Width:=Form4.Width;
end;
procedure TCaptureThread.send;
begin
Form1.Streamclient.IOHandler.Write(RawToBytes(Recblock,sizeof(recblock)),sizeof(recblock));
Form1.Streamclient.IOHandler.Write(stream,stream.Size,true);
end;
procedure TCaptureThread.showthumb;
begin
Form1.DesktopImage.Picture.Assign(bmp);
end;
procedure TCaptureThread.stretch;
begin
SetStretchBltMode(bmp.Canvas.Handle, HALFTONE);
StretchBlt(bmp.Canvas.Handle,0,0,thumbRect.Right,thumbRect.Bottom,bmp.Canvas.Handle,0,0,bmp.Width,bmp.Height,SRCCOPY);
end;
First in my delphi 2010 i must replace
unit CaptureUnit;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs;
with
unit CaptureUnit;
interface
uses
Windows, Messages, SysUtils, Variants,
Classes, Graphics, Controls, Forms, Dialogs;
The same also in unit.pas
You should not assign a Bitmap to Picture.Assign(bmp);
procedure TCaptureThread.showthumb;
begin
CaptureForm.DesktopImage.Picture.Assign(bmp);
end;
After a short time I get also an error EOutOfResources).
You should assign a Bitmap to Picture.Bitmap.Assign(bmp);
procedure TCaptureThread.showthumb;
begin
CaptureForm.DesktopImage.Picture.Bitmap.Assign(bmp);
end;
after I changed it, I got your program run for 20 minutes without getting an error. Then I finished it manually.
Update:
Screenshot : program running while Vcl Video playing and stretching and moving the Capture Area.
Hope it helps you.
Solved the problem. Wrote the code in Synchronize(), except PNG compression and before compression used method Canvas.Lock, after compression Canvas.UnLock. This allows you to avoid the influence of the another thread to Canvas. Thanks for bummi's advice(TCanvas is not threadsave). Correctly Execute method is here:
procedure TCaptureThread.Execute;
begin
inherited;
while not Terminated do
begin
Synchronize(size);
Synchronize(getscreen);
Synchronize(drawcursor);
try
png.Canvas.Lock;
bmp.Canvas.Lock;
png.Assign(bmp);
png.CompressionLevel := 9;
png.Canvas.Unlock;
bmp.Canvas.Unlock;
finally
end;
try
Synchronize(stretch);
Synchronize(showthumb);
finally
end;
sleep(200);
end;
end;
Related
I need to run multiples blocks of threads at same time. What Im trying to do is:
A have 197 operation do call individualy on shellExecute
I want to run 4 operations simultanelly
I can only start new operation if I have less then 4 executing
Problems:
I have to insert a lot of Application.ProcessMessages to make it work, and I dont know what Im doing wrong. I tryed everything and nothing seems to work.
Here is the code:
procedure TCompress.NewThread(psArgs: PWideChar);
var
oThread: TThread;
nCode : DWord;
begin
oThread := TThread.CreateAnonymousThread(
procedure
begin
try
FNumberOfThreads := FNumberOfThreads +1;
ExecuteAndWait(PChar(FsPathCompressor), psArgs, SW_HIDE, nCode);
//It is just a CreateProcess with WaitForSingleObject(retorno, INFINITE);
except on E: Exception do
begin
raise;
end;
end;
end);
oThread.OnTerminate := DoOnTerminate;
oThread.Start;
end;
procedure TspCompress.DoOnTerminate;
begin
FNumberOfThreads := FNumberOfThreads -1;
end;
function TspCompress.ExecuteBlocks: Boolean;
var
sArgs : WideString;
nBlocksCreated, nTotalBlocks: Integer;
begin
nTotalBlocks := 197;
nBlocksCreated := 0;
while nBlocksCreated < nTotalBlocks do
begin
//Needs Application.ProcessMessages to update FNumberOfThreads.
while (FNumberOfThreads < 4) and (nBlocksCreated < nTotalBlocks) do
begin
try
sArgs := PChar('C:/file.exe');
NewThread(PWideChar(sArgs));
//Needs Application.ProcessMessages to start the thread.
nBlocksCreated := nBlocksCreated + 1;
except
on E: Exception do
begin
//Do Something
end;
end;
end;
end;
end;
FNumberOfThreads is a private variable of the class
This is a sample code of what Im doing. The problem is not with the code it self, but with the Thread concept.
At the end, I just used System.Threading. Setting a ThreadPool and using Parellel.For.
procedure TCompress.MyParallelProcess;
var
sArgs : WideString;
nTotalProcess: Integer;
oPool: TThreadPool;
nCode: DWord;
begin
oPool := TThreadPool.Create;
try
oPool.SetMinWorkerThreads(4);
oPool.SetMaxWorkerThreads(4);
nTotalProcess := 197;
TParallel.For(1, nTotalProcess, procedure(i: integer)
begin
sArgs := PChar('C:/file'+IntToStr(i)+'.exe');
ExecuteAndWait(PChar(FsPathCompressor), sArgs, SW_HIDE, nCode);
end, oPool);
finally
FreeAndNil(oPool);
end;
end;
Remember, this is just a sample code, but i did something like this and works as a glove.
Thanks all for your help.
So I came across some code similar to this:
aThread := TmyThread.Create(param1, param2);
h := aThread.handle;
repeat
if (MsgWaitForMultipleObjects(1, h, False, INFINITE, QS_ALLINPUT) = WAIT_OBJECT_0)
then break;
Application.ProcessMessages
until False;
aThread.Free;
myProgressBar.Progress := myProgressBar.Max; //an internal component, not really important here
Which, I assume, is meant to provide a way of updating the GUI so it doesn't appear blocked, but also allow for the end process GUI to be updated (the progress bar), while some long task is taking place.
But it contains the dreaded Application.ProcessMessages.
I've read The Darkside of Application.ProcessMessages and many other Delphi blogs suggesting it's time to use a new thread instead when Application.ProcessMessages is being used.
So, is it wise to phase out this method of keeping the main/GUI threaded idle for something like the AnonymousThread approach shown here? Or something else?
This Noob is confused as to why it's suggested that a process that calls Application.ProcessMessages is a good candidate for a Thread, but the thread in question relies on the very thing we're being told not to do!
The main idea is not to wait for the thread. Thread should inform your form when it finished. In other words the code which should be executed after the the thread is finished should be isolated to a separate procedure (see TForm1.ThreadCompletedHandler) and thread should call it after it is finished.
Here is a small sample:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls;
type
TmyThread = class(TThread)
private
FParam1, FParam2: Integer;
FUpdateProc: TProc<TmyThread, Integer>;
FCompleteProc: TProc<TmyThread>;
procedure SyncCompleteProc;
procedure QueueUpdateProc(APosition: Integer);
protected
procedure Execute; override;
public
constructor Create(AParam1, AParam2: Integer;
AUpdateProc: TProc<TmyThread, Integer>;
ACompleteProc: TProc<TmyThread>);
end;
TForm1 = class(TForm)
Button1: TButton;
myProgressBar: TProgressBar;
procedure Button1Click(Sender: TObject);
private
FThread: TThread;
procedure ThreadUpdateHandler(AThread: TMyThread; APosition: Integer);
procedure ThreadCompletedHandler(AThread: TMyThread);
protected
procedure UpdateActions; override;
public
destructor Destroy; override;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TmyThread }
constructor TmyThread.Create(AParam1, AParam2: Integer; AUpdateProc: TProc<TMyThread, Integer>;
ACompleteProc: TProc<TMyThread>);
begin
FParam1 := AParam1;
FParam2 := AParam2;
FUpdateProc := AUpdateProc;
FCompleteProc := ACompleteProc;
inherited Create(False);
end;
procedure TmyThread.Execute;
var
I: Integer;
begin
//inherited; - abstract
try
I := FParam1;
while not Terminated and (I < FParam2) do
begin
Sleep(1000);
Inc(I);
QueueUpdateProc(I);
end;
finally
if Assigned(FCompleteProc) then
TThread.Queue(Self, SyncCompleteProc);
end;
end;
procedure TmyThread.QueueUpdateProc(APosition: Integer);
begin
if Terminated or not Assigned(FUpdateProc) then
Exit;
TThread.Queue(Self,
procedure
begin
FUpdateProc(Self, APosition);
end);
end;
procedure TmyThread.SyncCompleteProc;
begin
FCompleteProc(Self);
end;
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
const
param1 = 1;
param2 = 5;
begin
myProgressBar.Min := param1;
myProgressBar.Max := param2 + 1;
myProgressBar.Position := param1;
FThread := TmyThread.Create(param1, param2, ThreadUpdateHandler, ThreadCompletedHandler);
end;
destructor TForm1.Destroy;
begin
if Assigned(FThread) then
FThread.Terminate;
inherited;
end;
procedure TForm1.ThreadCompletedHandler(AThread: TmyThread);
begin
try
if not AThread.Terminated then // check form is not destroye yet
begin
FThread := nil;
myProgressBar.Position := myProgressBar.Max; //an internal component, not really important}
end;
finally
FreeAndNil(AThread);
end;
end;
procedure TForm1.ThreadUpdateHandler(AThread: TMyThread; APosition: Integer);
begin
if not AThread.Terminated then // check form is not destroye yet
myProgressBar.Position := APosition;
end;
procedure TForm1.UpdateActions;
begin
inherited;
Button1.Enabled := not Assigned(FThread);
end;
end.
and DFM file
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 290
ClientWidth = 554
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 24
Top = 16
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click
end
object myProgressBar: TProgressBar
Left = 120
Top = 108
Width = 150
Height = 17
TabOrder = 1
end
end
NB In this sample form does not wait for the thread so potentially we may close the application before the thread is terminated. It should not be a problem for this sample as it simple enough but in real live you may need to wait for the thread in Form.Destroy or create some thread manager which should wait for all running threads before application is finished.
I am trying to write a Delphi code to update the TImage components on a from. I was using C++ for long time but don't have much experience in Delphi;however, I need to use Delphi for some reason. I obtained and modified a similar code to do the task as shown as follows. I have 4 TImages components placed on a form, I start a thread to update the images when the player press the button. I have 6 resource bitmaps with numbered in 1 to 6 embedded in the project and the code will randomly pick and load the resource bitmap into the TImage. I find that using the thread sometimes, some images will not be drawn (just blank like no image being loaded). I know you may be wondering why I need to use a thread and why not just call ShowRanodmImage when the button is pressed. In this simple case, surely I don't have to use thread instead but I am going to apply to some situation that threading must be used. So my question is why the following code will occationally not show the proper image? How to fix that? Thanks a lot.
unit TTUnit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls;
type
TTestThread = class;
TTestForm = class(TForm)
NextBTN: TButton;
P11: TPanel;
P12: TPanel;
P13: TPanel;
P14: TPanel;
CELL11: TImage;
CELL12: TImage;
CELL22: TImage;
CELL21: TImage;
procedure FormCreate(Sender: TObject);
procedure NextBTNClick(Sender: TObject);
private
myThread :TTestThread;
pImgs: array[1..2, 1..2] of TImage;
public
procedure ShowRandomImg;
end;
TTestThread = class(TThread)
private
myForm :TTestForm;
protected
procedure Execute(); override;
public
constructor Create(aFrom :TTestForm; CreateSuspended: Boolean);
end;
var
TestForm: TTestForm;
implementation
{$R *.dfm}
procedure TTestForm.FormCreate(Sender: TObject);
begin
myThread := TTestThread.Create(Self, True);
pImgs[1][1]:=CELL11; pImgs[1][2]:=CELL12;
pImgs[2][1]:=CELL21; pImgs[2][2]:=CELL22;
end;
procedure TTestForm.NextBTNClick(Sender: TObject);
begin
myThread.Resume;
end;
procedure TTestForm.ShowRandomImg;
var
i, j :Integer;
r :array[1..2, 1..2] of Integer;
begin
Self.NextBTN.Enabled := false;
r[1][1] := Random(6)+1;
r[1][2] := Random(6)+1;
r[2][1] := Random(6)+1;
r[2][2] := Random(6)+1;
for i := 1 to 2 do
begin
for j := 1 to 2 do
begin
pImgs[i][j].Picture.Bitmap.LoadFromResourceID(HInstance, r[i][j]);
pImgs[i][j].Canvas.Font.Size := 38;
pImgs[i][j].Canvas.Font.Color := clRed;
pImgs[i][j].Canvas.Font.Style := [fsBold];
pImgs[i][j].Canvas.TextOut(20, 20, inttostr(r[i][j]));
end;
end;
Self.NextBTN.Enabled := true;
end;
////////////////////
constructor TTestThread.Create(aFrom: TTestForm; CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);
Self.myForm := aFrom;
end;
procedure TTestThread.Execute;
begin
inherited;
Repeat
Self.myForm.ShowRandomImg;
Self.Suspend;
Until Terminated;
end;
end.
I'm developing a multithread download application. I have one thread that creates many threads that download data. While downloading I need to see the progress in progress bar, so I set the maximum as the size of the file, and I calculate current downloaded data by using IdHTTPWork, which I added as a procedure of thread (secondary thread). When my app is started, the main thread creates other threads to download (in the loop for) and set the position of begin and end (idhttp.request.range), then each thread starts downloading like this:
HTTP.Request.Range := Format('%d-%d',[begin ,end]);
HTTP.Get(url,fs);
this is the procedure of secondarythread.work:
procedure TSecondaryThread.IdHTTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
if AWorkMode = wmRead then
position:= AWorkCount;// position is a global variable
SendMessage(HWND_BROADCAST,MyMessage, 2,position);
end;
I don't know if this is the right code, but I can't find another solution. Each thread can increment position using the value of downloaded data, so position will contain the global downloads in instant S, I don't know if this is true.
Now my questions:
1- the progress doesn't correspond to the current amount of downloaded data; instead, it increments very slowly.
2-when I add -just when I add- Asend message in this procedure, it never stops working!!
So what is the problem?
You have the right idea by giving each worker thread its own TIdHTTP object and its own OnWork event handler. But you are not delivering those status updates to the main thread correctly.
Use PostMessage() instead of SendMessage() so that you do not slow down your worker threads.
You have multiple worker threads posting status updates to the main thread, so DO NOT use a global variable to hold the progress, and certainly DO NOT have the worker threads update that variable directly. Each worker thread should put its current status directly in the parameters of the message that gets posted to the main thread, and then the main thread can have a private counter variable that it increments with each status update.
DO NOT post the status updates using HWND_BROADCAST - that broadcasts the message to every top-level window in the system! Post the messages only to your main thread, by posting to an HWND that belongs to the main thread (I would suggest using AllocateHWnd() for that).
Try something like this:
unit StatusUpdates;
uses
Windows;
interface
type
PStatus = ^TStatus;
TStatus = record
BytesDownloadedThisTime: Int64;
BytesDownloadedSoFar: Int64;
MaxBytesBeingDownloaded: Int64;
end;
var
StatusUpdateWnd: HWND = 0;
implementation
end.
uses
..., StatusUpdates;
type
TMainForm = class(TForm)
...
private
TotalDownloaded: Int64;
...
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
StatusUpdateWnd := AllocateHWnd(StatusWndProc);
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
if StatusUpdateWnd <> 0 then
begin
DeallocateHWnd(StatusUpdateWnd);
StatusUpdateWnd := 0;
end;
end;
procedure TMainForm.StartDownload;
begin
ProgressBar1.Position := 0;
ProgressBar1.Max := FileSizeToBeDownloaded;
TotalDownloaded := 0;
// create download threads...
end;
procedure TMainForm.StatusWndProc(var Message: TMessage);
var
Status: PStatus;
begin
if Message.Msg = MyMessage then
begin
Status := PStatus(Message.LParam);
try
if Status.BytesDownloadedThisTime > 0 then
begin
Inc(TotalDownloaded, Status.BytesDownloadedThisTime);
ProgressBar1.Position := TotalDownloaded;
end;
// use Status for other things as needed...
finally
Dispose(Status);
end;
end else
Message.Result := DefWindowProc(StatusUpdateWnd, Message.Msg, Message.WParam, Message.LParam);
end;
uses
..., StatusUpdates;
type
TSecondaryThread = class(TThread)
private
FTotalBytes: Int64;
FMaxBytes: Int64;
procedure PostStatus(BytesThisTime: Int64);
...
end;
procedure TSecondaryThread.PostStatus(BytesThisTime: Int64);
var
Status: PStatus;
begin
New(Status);
Status.BytesDownloadedThisTime := BytesThisTime;
Status.BytesDownloadedSoFar := FTotalBytes;
Status.MaxBytesBeingDownloaded := FMaxBytes;
if not PostMessage(StatusUpdateWnd, MyMessage, 2, LPARAM(Status)) then
Dispose(Status);
end;
procedure TSecondaryThread.IdHTTPWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
begin
if AWorkMode = wmRead then
begin
FTotalBytes := 0;
FMaxBytes := AWorkCountMax;
PostStatus(0);
end;
end;
procedure TSecondaryThread.IdHTTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
var
BytesThisTime: Int64;
begin
if AWorkMode = wmRead then
begin
BytesThisTime := AWorkCount - FTotalBytes;
FTotalBytes := AWorkCount;
PostStatus(BytesThisTime);
end;
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.