tIdHttp Inside thread and IdTCPServer in GUI - multithreading

I have a TTimer on a TForm, where the timer is set to 5 seconds and creates 100 threads to fetch XML from a remote server.
Each time a thread is executed, I add the XML to a variable (FullXML_STR:String).
When all threads have finished, I am sending the FullXML_STR to all Clients connected to a TIdTCPServer.
unit Unit1;
interface
uses
IdGlobal,IdContext, system.win.Comobj, system.syncObjs, MSXML2_TLB, activex,
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IdBaseComponent, IdComponent,
IdTCPConnection, IdTCPClient, IdHTTP, IdCustomTCPServer, IdCustomHTTPServer,
IdHTTPServer, Vcl.ExtCtrls;
Type
TxClientThread = class(TThread)
private
fHttpClient: TIdHTTP;
furl: String;
ftag:Integer;
fResponseXML:String;
fXML: IXMLDOMDocument;
fNode: IXMLDomNode;
protected
procedure Execute; override;
procedure DoTerminate; override; **//Added**
public
constructor Create(atag:Integer;AURL:string);reintroduce;
destructor Destroy; override;
end;
type
TForm1 = class(TForm)
IdTCPServer1: TIdHTTPServer;
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure StartTimerAgain;
end;
const
maximumThreads=200;
var
Form1: TForm1;
Threads_downloaded:Integer;
Total_threads:Integer;
FullXML_STR:String;
Clients:TList;
CriticalSection:TCriticalSection;
ClientThread:Array[0..maximumThreads] of TxClientThread;
implementation
{$R *.dfm}
{TxClientThread}
constructor TxClientThread.Create(atag:Integer;AURL:string);
begin
inherited Create(false);
furl:=Aurl;
ftag:=Atag;
fResponseXML:='';
fHttpClient := TIdHTTP.Create(nil);
fHttpClient.Tag:=ftag;
fHttpClient.ConnectTimeout:=60000;
fHttpClient.ReadTimeout:=60000;
fHttpClient.Request.Accept:='*/*';
fHttpClient.Request.UserAgent:='Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36';
FreeOnTerminate := True;
end;
destructor TxClientThread.Destroy;
begin
fHttpClient.Free;
inherited Destroy;
end;
procedure TxClientThread.Execute;
begin
try
fResponseXML:= fHttpClient.Get(furl);
except
end;
end;
procedure TxClientThread.DoTerminate;
begin
inc(Threads_downloaded);
///****** parsing The XML
try
CoInitialize(nil);
fXML := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
fXML.async := false;
try
fXML.loadXML(fResponseXML);
fNode := fXML.selectSingleNode('/games');
if fNode<>nil then
begin
FullXML_STR:=FullXML_STR + fNode.attributes.getNamedItem('id').text+'^';
end;
finally
fxml:=nil; //---> do i need this?
end;
finally
CoUninitialize;
end;
if Threads_downloaded=Total_threads then
begin
TThread.Synchronize(nil,procedure/////////Sould i USe This or Synchronize
var
i:Integer;
begin
CriticalSection.enter;
if not Assigned(Form1.IdTCPServer1.Contexts) then exit;
try
Clients:=Form1.IdTCPServer1.Contexts.LockList;
try
for i:=pred(Clients.Count) downto 0 do
try
TIdContext(Clients[i]).Connection.IOHandler.Writeln(FullXML_STR,IndyTextEncoding_UTF8);
except
end;
finally
Form1.IdTCPServer1.Contexts.UnlockList;
end;
finally
CriticalSection.leave;
end;
form1.StartTimerAgain; ///Startinmg againe Then timer
end
);
end;
/////////// End \ All threads downloaded
inherited;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
CriticalSection:=TCriticalSection.create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
CriticalSection.Free;
end;
procedure tform1.StartTimerAgain;
begin
Form1.Timer1.Enabled:=true
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
x:Integer;
aUrl:String;
begin
FullXML_STR:='';
Timer1.Enabled:=false;
Threads_downloaded:=0;
Total_threads=100;
for x:=0 to Pred(Total_threads) do
begin
aUrl:='http://example.com/myxml'+Formatfloat('0',x)+'.xml';
ClientThread[Threads_downloaded]:=TxClientThread.Create(x,aUrl);
end;
end;
end.
main problem is that after 1-2 Hours programm is not responding.
in each thread's Execute(), I check if all Threads have finished downloading. Is there a better way to know that all my threads are finished?
is it better to call Contexts.LockList() on the TIdTCPServer before the timer starts creating the threads, and unlock it after the threads are finished?
What can I do to optimize my code so I can be sure that the timer will be alive all the time? I am restarting the timer after all threads are finished.
Is this the correct way to do it?
Request:
How is it possible to accept a string like hi from a client connected on the TIdTCPServer and send back a string.
I try to add the following code:
var
RxBuf: TIdBytes;
Data := TxClientContext(AContext).ExtractQueuedStrings;
if Data <> nil then
try
for i := 0 to Pred(Data.Count) do
AContext.Connection.IOHandler.WriteLn(Data[i]);
finally
Data.Free;
end;
RxBuf := nil;
with AContext.Connection do
begin
IOHandler.CheckForDataOnSource(100);
if not IOHandler.InputBufferIsEmpty then
begin
InputBuffer.ExtractToBytes(RxBuf); //for TIdBytes
AContext.Connection.IOHandler.WriteLn('hello');
end;
end;
After sending hello the app never sends data from the queue.
How can I add the hello to Data extract from queue?
Something like this:
Data := TxClientContext(AContext).ExtractQueuedStrings;
and then
data.text:=data.text +'hello data';
or how can I add the 'hello data' in the queue?

I see a lot of mistakes in your code. Rather than pointing them out individually, I would suggest just rewritting the entire code, especially since you are also asking for optimizations.
Try something more like this instead:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls,
IdGlobal, IdContext, IdBaseComponent, IdComponent, IdTCPConnection, IdCustomTCPServer,
IdTCPServer, IdThreadSafe;
type
TIdTCPServer = class(IdTCPServer.TIdTCPServer)
protected
procedure DoTerminateContext(AContext: TIdContext); override;
end;
TForm1 = class(TForm)
IdTCPServer1: TIdTCPServer;
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure IdTCPServer1Connect(AContext: TIdContext);
procedure IdTCPServer1Execute(AContext: TIdContext);
private
{ Private declarations }
IDs: TIdThreadSafeString;
Threads: TList;
procedure ThreadTerminated(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
System.Win.Comobj, MSXML2_TLB, ActiveX, System.SyncObjs, IdHTTP, IdYarn;
{$R *.dfm}
const
maximumThreads = 100;//200;
{TxClientContext}
type
TxClientContext = class(TIdServerContext)
private
fQueue: TIdThreadSafeStringList;
fInQueue: TEvent;
public
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
destructor Destroy; override;
procedure AddStringToQueue(const S: string);
function ExtractQueuedStrings: TStrings;
end;
constructor TxClientContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil);
begin
inherited;
fQueue := TIdThreadSafeStringList.Create;
fInQueue := TEvent.Create(nil, True, False, '');
end;
destructor TxClientContext.Destroy; override;
begin
fQueue.Free;
fInQueue.Free;
inherited;
end;
procedure TxClientContext.AddStringToQueue(const S: string);
var
List: TStringList;
begin
List := fQueue.Lock;
try
List.Add(S);
fInQueue.SetEvent;
finally
fQueue.Unlock;
end;
end;
function TxClientContext.ExtractQueuedStrings: TStrings;
var
List: TStringList;
begin
Result := nil;
if fInQueue.WaitFor(INFINITE) <> wrSignaled then Exit;
List := FQueue.Lock;
try
if List.Count > 0 then
begin
Result := TStringList.Create;
try
Result.Assign(List);
List.Clear;
except
Result.Free;
raise;
end;
end;
fInQueue.ResetEvent;
finally
fQueue.Unlock;
end;
end;
{TxClientThread}
type
TxClientThread = class(TThread)
private
fURL: String;
protected
procedure Execute; override;
public
GameID: string;
constructor Create(AURL: string; AOnTerminate: TNotifyEvent); reintroduce;
end;
constructor TxClientThread.Create(AURL: string; AOnTerminate: TNotifyEvent);
begin
inherited Create(False);
fURL := AURL;
OnTerminate := AOnTerminate;
FreeOnTerminate := True;
end;
procedure TxClientThread.Execute;
var
HttpClient: TIdHTTP;
ResponseXML: String;
XML: IXMLDOMDocument;
Node: IXMLDomNode;
begin
HttpClient := TIdHTTP.Create(nil);
try
HttpClient.ConnectTimeout := 60000;
HttpClient.ReadTimeout := 60000;
HttpClient.Request.Accept := '*/*';
HttpClient.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36';
ResponseXML := HttpClient.Get(fURL);
finally
HttpClient.Free;
end;
CoInitialize(nil);
try
XML := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
try
XML.async := False;
XML.loadXML(ResponseXML);
Node := XML.selectSingleNode('/games');
if Node <> nil then
try
GameID := Node.attributes.getNamedItem('id').text;
finally
Node := nil;
end;
finally
XML := nil;
end;
finally
CoUninitialize;
end;
end;
{TIdTCPServer}
procedure TIdTCPServer.DoTerminateContext(AContext: TIdContext);
begin
inherited; // <-- closes the socket
TxClientContext(AContext).FInQueue.SetEvent; // unblock OnExecute if it is waiting for data...
end;
{TForm1}
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TxClientContext;
IDs := TIdThreadSafeString.Create;
Threads := TList.Create;
Threads.Capacity := maximumThreads;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
IDs.Free;
Threads.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
x: Integer;
Thread: TxClientThread;
begin
Timer1.Enabled := False;
IDs.Value := '';
for x := 0 to Pred(maximumThreads) do
begin
Thread := TxClientThread.Create('http://example.com/myxml' + IntToStr(x) + '.xml', ThreadTerminated);
try
Threads.Add(TObject(Thread));
except
Thread.Free;
raise;
end;
end;
end;
proccedure TForm1.ThreadTerminated(Sender: TObject);
var
Clients: TList;
s: string;
i: Integer;
begin
try
s := TxClientThread(Sender).GameID;
if s <> '' then IDs.Append(s + '^');
finally
Threads.Remove(Sender);
end;
if (Threads.Count > 0) or (not Assigned(IdTCPServer1.Contexts)) then Exit;
s := IDs.Value;
if s = '' then Exit;
Clients := IdTCPServer1.Contexts.LockList;
try
for i := Pred(Clients.Count) downto 0 do
try
TxClientContext(TIdContext(Clients[i])).AddStringToQueue(s);
except
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
Timer1.Enabled := True;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Data: TStrings;
i: Integer;
begin
Data := TxClientContext(AContext).ExtractQueuedStrings;
if Data <> nil then
try
for i := 0 to Pred(Data.Count) do
AContext.Connection.IOHandler.WriteLn(Data[i]);
finally
Data.Free;
end;
end;
end.

In each thread, you add the resulting string into a global variable. That is not a safe operation. Instead, add an OnTerminate handler to your threads, where you add the result and also can keep track of the threads.
This is safe, since the OnTerminate handler is executed in the main thread.
I suggest to pass a callback method to pass the result. It is declared like:
type
TSyncMethod = procedure(const ReturnValue: String) of object;
Change the thread accordingly:
Type
TxClientThread = class(TThread)
private
furl : String;
ftag : Integer;
fCallbackMethod : TSyncMethod;
fXMLResult : String;
procedure AfterWork(Sender : TObject);
...
public
constructor Create(atag: Integer; AURL: string; CallbackMethod : TSyncMethod); reintroduce;
...
end;
Add a callback method to your form:
Type
TForm1 = Class(TForm1)
private
// Put your "global" variables here
Threads_downloaded : Integer;
Total_threads : Integer;
FullXML_STR : String;
procedure ManageThreadReturnValue(const ReturnValue : String); // Callback from threads
...
end;
The implementation part:
constructor TxClientThread.Create(atag: Integer; AURL: string; CallbackMethod : TSyncMethod);
begin
inherited Create(false);
furl := Aurl;
ftag := Atag;
fCallbackMethod := CallbackMethod;
fXMLResult := '';
OnTerminate := AfterWork; // Execute AfterWork when thread terminates (in main thread)
FreeOnTerminate := True;
end;
procedure TxClientThread.Execute;
var
lHttpClient : TIdHTTP;
lResponseXML :String;
lXML : IXMLDOMDocument;
lNode : IXMLDomNode;
begin
lHttpClient := TIdHTTP.Create(nil);
try
lHttpClient.Tag := ftag;
lHttpClient.ConnectTimeout := 60000;
lHttpClient.ReadTimeout := 60000;
lHttpClient.Request.Accept := '*/*';
lHttpClient.Request.UserAgent :=
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36';
try
lResponseXML:= lHttpClient.Get(fUrl);
except
end;
finally
lHttpClient.Free;
end;
///****** parsing The XML
CoInitialize(nil);
try
lXML := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
lXML.async := false;
try
lXML.loadXML(lResponseXML);
lNode := lXML.selectSingleNode('/games');
if lNode<>nil then
begin
fXMLResult := lNode.attributes.getNamedItem('id').text+'^';
end;
finally
lnode := nil;
lxml := nil; //---> Q: do i need this?
//---> A: Yes, it must be finalized before CoUnitialize
end;
finally
CoUninitialize;
end;
end;
procedure TxClientThread.AfterWork;
begin
if Assigned(fCallbackMethod) then
fCallbackMethod(fXMLResult); // Pass data
end;
procedure TForm1.ManageThreadReturnValue(const ReturnValue : String);
var
i : Integer;
Clients : TList;
begin
// Take care of the return value and other things related to
// what happens when a thread ends.
FullXML_STR := FullXML_STR + ReturnValue;
Inc(threads_downloaded);
if Threads_downloaded = Total_threads then
begin
if Assigned(IdTCPServer1.Contexts) then
begin
Clients:= IdTCPServer1.Contexts.LockList;
try
for i:= Pred(Clients.Count) downto 0 do
begin
try
TIdContext(Clients[i]).Connection.IOHandler.Writeln(
FullXML_STR,IndyTextEncoding_UTF8);
except
end;
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
StartTimerAgain; ///Starting again The timer
end;
end;
// Initiate threads
FullXML_STR:='';
Timer1.Enabled:=false;
Threads_downloaded:=0;
Total_threads=100;
for x:= 0 to Pred(Total_threads) do
begin
aUrl:='http://example.com/myxml'+Formatfloat('0',x)+'.xml';
TxClientThread.Create(x,aUrl,ManageThreadReturnValue); // !! Never keep a reference to a thread with FreeOnTerminate = true
end;
Some other hints:
Put your global variables into the private section of TForm1. This is the place where they belong.
Remove the ClientThread array, since a reference to a thread with FreeOnTerminate = true should never be used.
Do not swallow exceptions, i.e. empty except end clauses are not a good practice.
By using the callback method, you decouple the thread from code/data that does not belong to the thread. That is one of the most important lessons to learn when programming (i.e. avoid making spaghetti code).

Related

Image Download through thread error

Happy new year to all StackOverFlow member and readers !
I come to you today for a question regarding threads in Delphi (I browsed most of what was already posted on the subject but could not find a clue).
I have a very simple test application with one Form (frmIMGDown) and a thread unit.
On the form are found
a Tbutton
a TImage
a TprogressBar
When clicked, the button starts a thread that downloads an image from the web, updates the progressbar during the process and displays a downloaded image in the Timage.
This works fine as long as the calling Form (frmIMGDown) is the main application form, OR if it is called from another form but all forms are created on application start.
Now, if I dynamically create frmIMGDown from a button click on the Main Form with :
procedure TForm1.Button2Click(Sender: TObject);
var
frmIMGDown : TfrmIMGDown;
begin
try
frmIMGDown := TfrmIMGDown.Create(nil);
frmIMGDown.ShowModal;
finally
frmIMGDown.Free;
end;
end;
I get an Access Violation at address... error
If I change
frmIMGDown := TfrmIMGDown.Create(nil);
to
frmIMGDown := TfrmIMGDown.Create(Form1);
the result is the same with the same error.
I suspect this has to do with the thread I implemented and maybe the variables used and that I try to send back to frmIMGDown, but I can't find the solution.
Here is the thread unit :
unit unit_MyThread;
interface
uses
Classes, IdHTTP, VCL.Forms, SyStem.UITypes, SysUtils, VCL.Dialogs, Graphics, IdTCPClient, IdTCPConnection, IdComponent,IdBaseComponent;
type
TIdHTTPThread = class(TThread)
private
FURL : String;
idHTTP: TIdHTTP;
B : TBitMap;
W : TWICImage;
//MS : TMemoryStream;
public
Constructor Create(CreateSuspended: Boolean);
Destructor Destroy; override;
Property URL : String read FURL WRITE FURL;
procedure OnWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
procedure OnWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
procedure OnWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
protected
procedure Execute; override;
end;
implementation
uses
unit_IMG_Down;
Constructor TiDHTTPThread.Create(CreateSuspended: Boolean);
begin
inherited Create(Suspended);
IdHTTP := TIdHTTP.Create;
Screen.Cursor := crHourGlass;
IdHTTP.onWork := OnWork;
IdHTTP.OnWorkbegin := OnWorkBegin;
IdHTTP.OnWorkEnd := OnWorkEnd;
B := TBitmap.Create;
W := TWICImage.Create;
end;
Destructor TIdHTTPThread.Destroy;
begin
idHTTP.Free;
B.Free;
W.Free;
Screen.Cursor := crDefault;
inherited Destroy;
end;
procedure TIdHTTPThread.Execute;
var
MS : TMemoryStream;
begin
Screen.Cursor := crHourGlass;
try
MS := TMemoryStream.Create;
try
IdHTTP.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';
IdHTTP.Get(URL,MS);
MS.Position := 0;
W.LoadFromStream(MS);
B.Assign(W);
frmIMGDown.Image3.Picture.Assign(B);
except
On E: Exception do ShowMessage(E.Message);
end;
finally
MS.Free;
end;
end;
procedure TIdHTTPThread.OnWork(ASender: TObject; AWorkMode: TWorkMode;
AWorkCount: Int64);
var
Http: TIdHTTP;
ContentLength: Int64;
Percent: Integer;
begin
Http := TIdHTTP(ASender);
ContentLength := Http.Response.ContentLength;
if (Pos('chunked', LowerCase(Http.Response.TransferEncoding)) = 0) and
(ContentLength > 0) then
begin
Percent := 100*AWorkCount div ContentLength;
frmIMGDown.ProgressBar3.Position := AWorkCount +2;
frmIMGDown.ProgressBar3.Position := AWorkCount -1;
end;
end;
procedure TIdHTTPThread.OnWorkBegin(ASender: TObject; AWorkMode: TWorkMode;
AWorkCountMax: Int64);
begin
frmIMGDown.ProgressBar3.Visible := True;
frmIMGDown.ProgressBar3.Position := 0;
end;
procedure TIdHTTPThread.OnWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
frmIMGDown.ProgressBar3.Visible := false;
end;
end.
And the call to thread from the button
procedure TfrmIMGDown.Button3Click(Sender: TObject);
var
HTTPThread : TIdHTTPThread;
begin
HTTPThread := TIdHTTPThread.Create(False);
HTTPThread.URL := 'https://bw-1651cf0d2f737d7adeab84d339dbabd3-bcs.s3.amazonaws.com/products/product_119522/Full119522_283b3acc91f119ab4b2939b1beb67211.jpg';
HTTPThread.FreeOnTerminate := True;
end;
SIDE NOTE : I used TWICImage to download the image (LoadFromStream) because I don't known which format the image will be (here the URl is hard-coded for the test) and assign it to a TBitmap after that.
Thanks in advance and, again, a happy new year to all.
Math
Your thread is accessing the Form's global pointer variable. When you get the Access Violation error, it is because you are not assigning the new Form object to that global variable, you are assigning it to a local variable of the same name instead. So the global pointer is invalid when the thread tries to access it.
The solution is to have the Form object pass its Self pointer to the thread, and then store it in a member of the thread. Don't rely on the global pointer at all.
A better solution is to not let the thread know anything about the UI at all. I would suggest defining events in the thread class, and have the thread fire those events when needed (image downloaded, progress updates, errors, etc). Then the Form can assign handlers to those events to update the UI as needed.
Also, your thread is not synchronizing with the main thread when accessing the Form's UI controls. The VCL is not thread-safe, so you MUST synchronize access to the UI. Even TBitmap is not thread-safe (not sure about TWICImage), you must Lock its Canvas when working with it in a thread, and Unlock when done.
Also, you have a race condition, as you are allowing the thread to (potentially) start running before you have assigned its URL and FreeOnTerminated values. You need to create the thread in a suspended state and not start it running until after you finish initializing it. The best way to do that is to create the thread with CreateSuspended=False and handle all of the initializations in the thread's constructor itself. The thread will not start running until its constructor exits. Otherwise, create the thread with CreateSuspended=True and explicitly resume it when ready.
With all of that said, try something more like this:
unit unit_MyThread;
interface
uses
Classes, IdComponent, IdBaseComponent;
type
THTTPStage = (HTTPInit, HTTPDownloading, HTTPDone);
THTTPStatusEvent = procedure(Sender: TObject; Progress, Total: Int64; Stage: THTTPStage) of object;
THTTPImageEvent = procedure(Sender: TObject; Data: TStream) of object;
THTTPThread = class(TThread)
private
FURL : String;
FStream : TMemoryStream;
FProgress, FTotal : Int64;
FStage : THTTPStage;
FOnStatus : THTTPStatusEvent;
FOnImage : THTTPImageEvent;
procedure DoOnStatus;
procedure DoOnImage;
procedure HTTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
procedure HTTPWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
procedure HTTPWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
protected
procedure Execute; override;
public
constructor Create(const AURL: string);
property OnStatus: THTTPStatusEvent read FOnStatus write FOnStatus;
property OnImage: THTTPImageEvent read FOnImage write FOnImage;
end;
implementation
uses
IdTCPClient, IdTCPConnection, IdHTTP;
constructor THTTPThread.Create(const AURL: string);
begin
inherited Create(True);
FreeOnTerminate := True;
FURL := AURL;
end;
procedure THTTPThread.Execute;
var
IdHTTP: TIdHTTP;
begin
IdHTTP := TIdHTTP.Create;
try
IdHTTP.OnWork := HTTPWork;
IdHTTP.OnWorkBegin := HTTPWorkBegin;
IdHTTP.OnWorkEnd := HTTPWorkEnd;
IdHTTP.Request.UserAgent := 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0';
FStream := TMemoryStream.Create;
try
IdHTTP.Get(FURL, FStream);
FStream.Position := 0;
if Assigned(FOnImage) then
Synchronize(DoOnImage);
finally
FStream.Free;
end;
finally
IdHTTP.Free;
end;
end;
procedure THTTPThread.DoOnStatus;
begin
if Assigned(FOnStatus) then
FOnStatus(Self, FProgress, FTotal, FStage);
end;
procedure THTTPThread.DoOnImage;
begin
if Assigned(FOnImage) then
FOnImage(Self, FStream);
end;
procedure THTTPThread.HTTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
if AWorkMode = wmRead then
begin
FProgress := AWorkCount;
FStage := HTTPDownloading;
if Assigned(FOnStatus) then
Synchronize(DoOnStatus);
end;
end;
procedure THTTPThread.HTTPWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
begin
if AWorkMode = wmRead then
begin
FProgress := 0;
FTotal := AWorkCountMax;
FStage := HTTPInit;
if Assigned(FOnStatus) then
Synchronize(DoOnStatus);
end;
end;
procedure THTTPThread.HTTPWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
if AWorkMode = wmRead then
begin
FProgress := FTotal;
FStage := HTTPDone;
if Assigned(FOnStatus) then
Synchronize(DoOnStatus);
end;
end;
end.
procedure TfrmIMGDown.Button3Click(Sender: TObject);
var
HTTPThread : THTTPThread;
begin
HTTPThread := THTTPThread.Create('https://bw-1651cf0d2f737d7adeab84d339dbabd3-bcs.s3.amazonaws.com/products/product_119522/Full119522_283b3acc91f119ab4b2939b1beb67211.jpg');
HTTPThread.OnStatus := HTTPStatus;
HTTPThread.OnImage := HTTPImage;
HTTPThread.OnTerminate := HTTPTerminated;
HTTPThread.Resume;
end;
procedure TfrmIMGDown.HTTPStatus(Sender: TObject; Progress, Total: Int64; Stage: THTTPStage);
begin
case Stage of
HTTPInit: begin
ProgressBar3.Visible := True;
ProgressBar3.Position := 0;
ProgressBar3.Max := 100;
Screen.Cursor := crHourGlass;
end;
HTTPDownloading: begin
if Total <> 0 then
ProgressBar3.Position := 100*Progress div Total;
end;
HTTPDone: begin
ProgressBar3.Visible := false;
Screen.Cursor := crDefault;
end;
end;
procedure TfrmIMGDown.HTTPImage(Sender: TObject; Data: TStream);
var
J: TJPEGImage;
begin
J := TJPEGImage.Create;
try
J.LoadFromStream(Data);
Image3.Picture.Assign(J);
finally
J.Free;
end;
end;
procedure TfrmIMGDown.HTTPTerminated(Sender: TObject);
begin
if TThread(Sender).FatalException <> nil then
ShowMessage(Exception(TThread(Sender).FatalException).Message);
end;

TThread checksynchronize issue with dll

i have this TThread that i use inside my dll to update some visual control its working fine but i face issue when i try to close my dll and reopen it again its raised this exception
checksynchronize called from thread which is not the main thread
what iam doing wrong ? i need to call checksynchronize within timer because i will update some vcl with Threading while app running .
Here is my Thread unit
unit Thread;
interface
uses Messages, Windows, SysUtils, dialogs, Classes, Menus, forms, ComOBJ,
ShlObj;
{ Thread client }
type
TThreadCallbackProc = procedure(Sender: TObject; Updatestring : string) of object;
TAPPTHREAD = class(TThread)
private
Fstatus : String;
FOnCallbackProc: TThreadCallbackProc;
procedure dosomework;
procedure DoCallbackProc;
//
protected
procedure Execute; override;
Public
constructor Create(CreateSuspended: Boolean; aThreadCallbackProc: TThreadCallbackProc);
destructor Destroy; override;
end;
var
APPTHREAD : TAPPTHREAD;
implementation
constructor TAPPTHREAD.Create(CreateSuspended: Boolean;
aThreadCallbackProc: TThreadCallbackProc);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
FOnCallbackProc := aThreadCallbackProc;
end;
destructor TAPPTHREAD.Destroy;
begin
//
end;
procedure TAPPTHREAD.DoCallbackProc;
begin
if Assigned(FOnCallbackProc) then
FOnCallbackProc(self, Fstatus);
end;
procedure TAPPTHREAD.Execute;
begin
while not Terminated do
begin
Fstatus := 'Synched';
if Fstatus <> '' then
dosomework;
end;
end;
procedure TAPPTHREAD.dosomework;
begin
if Assigned(FOnCallbackProc) then
begin
Synchronize(DoCallbackProc);
end;
end;
end.
Main Form
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.ExtCtrls;
type
TForm1 = class(TForm)
Label1: TLabel;
Timer1: TTimer;
Timer2: TTimer;
procedure FormShow(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer2Timer(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
procedure callbackproc(Sender: TObject; Updatestring : String);
end;
var
Form1: TForm1;
implementation
uses Thread;
{$R *.dfm}
procedure TForm1.callbackproc(Sender: TObject; Updatestring: String);
begin
label1.Caption := updatestring;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := Cafree;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
try
if Assigned(APPTHREAD) then
AppThread.Terminate;
except end;
try
Timer2.Enabled := False;
except end;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
APPTHREAD := TAPPTHREAD.Create(false, CallbackProc);
Timer2.Enabled := True;
end;
procedure TForm1.Timer2Timer(Sender: TObject);
begin
Checksynchronize;
end;
end.
DFM
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 242
ClientWidth = 472
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnClose = FormClose
OnDestroy = FormDestroy
OnShow = FormShow
PixelsPerInch = 96
TextHeight = 13
object Label1: TLabel
Left = 0
Top = 0
Width = 472
Height = 13
Align = alTop
Caption = 'Label1'
ExplicitLeft = 232
ExplicitTop = 136
ExplicitWidth = 31
end
object Timer1: TTimer
Enabled = False
OnTimer = Timer1Timer
Left = 232
Top = 128
end
object Timer2: TTimer
Enabled = False
Interval = 1
OnTimer = Timer2Timer
Left = 320
Top = 168
end
end
dll code
library dllapp;
uses
System.SysUtils,
Themes,
Windows,
Forms,
dialogs,
Graphics,
Vcl.ExtCtrls,
Unit1 in 'Unit1.pas' {Unit1},
DThreadsend in 'Thread.pas';
var
mHandle: THandle;
DLLHandle: Longint = 0;
function createApp(Width: Integer; Height: Integer; hw: HWnd;
app: TApplication): boolean; stdcall;
begin
mHandle := CreateMutex(nil, True, 'APPNAMETLOAD');
if GetLastError = ERROR_ALREADY_EXISTS then
begin
Halt;
end;
try
form1 := Tform1.CreateParented(hw); // **
form1.Width := Width;
form1.Height := Height;
Result := True
except
on e: exception do
begin
Result := False;
end;
end;
end;
procedure closeApp; stdcall;
begin
ApplicationClosed := True;
try
if mHandle <> 0 then
CloseHandle(mHandle);
except
end;
if Assigned(form1) then
try
FreeAndNil(form1);
except
end;
try
OptimizeRamUsage;
except
end;
end;
procedure showapp; stdcall;
begin
try
form1.Visible := True;
except
end;
form1.Show;
end;
procedure DLLEntryProc(EntryCode: Integer);
begin
case EntryCode of
DLL_PROCESS_DETACH:
begin
StyleServices.Free;
end;
DLL_PROCESS_ATTACH:
begin
end;
DLL_THREAD_ATTACH:
begin
end;
DLL_THREAD_DETACH:
begin
end;
end;
end;
exports
closeApp,
createApp,
showapp;
begin
DllProc := #DLLEntryProc;
end.
Host Application and how i create Dll
loadapp Unit
unit loadapp;
interface
uses windows, forms, System.SysUtils , dialogs;
procedure loadmainapp;
type
TcreaFunc = function (Width: Integer; Height: Integer; hw:HWnd; app: TApplication): boolean; stdcall;
TshowFunc = procedure stdcall;
TCloseAppFunc = procedure stdcall;
var
dllHandle : THandle = 0;
creaFunc : TcreaFunc;
showFunc : TshowFunc;
CloseAppFunc: TCloseAppFunc;
implementation
uses Mainapp;
procedure loadmainapp;
var
S: widestring;
PW: PWideChar;
begin
S := 'dllapp.dll';
pw:=pwidechar(widestring(s));
dllHandle := LoadLibrary(pw);
if dllHandle <> 0 then
begin
#creaFunc := GetProcAddress(dllHandle, 'createApp');
#showFunc := GetProcAddress(dllHandle, 'showapp');
if Assigned (creaFunc) then
begin
creaFunc(mainfrm.panel1.Width, mainfrm.panel1.Height, mainfrm.panel1.Handle, Application);
DisFunc;
end
else
ShowMessage('ERROR');
end
else
begin
ShowMessage('ERROR');
end;
end;
end.
Active Form
unit activeform;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ActiveX, AxCtrls, Frmldr_TLB, StdVcl, Vcl.ExtCtrls, ShlObj, Vcl.StdCtrls, SHDocVw, MSHTML;
type
TActiveFrmldr = class(TActiveForm, IActiveFrmldr)
mpanl: TPanel;
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
end;
implementation
uses ComObj, ComServ, Mainapp, libacload;
{$R *.DFM}
{ TActiveFrmldr }
procedure TActiveFrmldr.FormDestroy(Sender: TObject);
begin
if dllHandle <> 0 then
begin
#CloseAppFunc := GetProcAddress(dllHandle, 'closeApp');
CloseAppFunc;
FreeLibrary(dllHandle); //release dll
end;
if Assigned(mainfrm) then
try
FreeAndNil(mainfrm);
except
end;
end;
procedure TActiveFrmldr.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
mainfrm.Parent := mpanl;
mainfrm.Left := 0;
mainfrm.Top := 0;
mainfrm.Width := self.Width;
mainfrm.Height := self.Height;
mainfrm.Align := alClient;
mainfrm.Show;
end;
procedure TActiveFrmldr.FormCreate(Sender: TObject);
begin
Application.CreateForm(Tmainfrm, mainfrm);
Timer1.Enabled := True;
end;
initialization
TActiveFormFactory.Create(
ComServer,
TActiveFormControl,
TActiveFrmldr,
Class_ActiveFrmldr,
0,
'',
OLEMISC_SIMPLEFRAME or OLEMISC_ACTSLIKELABEL,
tmApartment);
finalization
end.
Main app Form that call load library function
unit Mainapp;
interface
uses
Windows, Messages, System.SysUtils, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, System.Classes, libacload,
Vcl.Controls, Vcl.StdCtrls;
type
Tmainfrm = class(TForm)
Panel1: TPanel;
Timer1: TTimer;
Timer2: TTimer;
procedure FormShow(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure Timer2Timer(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
mainfrm: Tmainfrm;
implementation
Uses loadapp;
{$R *.dfm}
procedure Tmainfrm.FormShow(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure Tmainfrm.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
loadmainapp;
end;
procedure Tmainfrm.Timer2Timer(Sender: TObject);
begin
checksynchronize; // i do this to check some thread in activex it self
end;
end.
The error means that CheckSynchronize() is being called in a thread whose ThreadID does not match the RTL's global System.MainThreadID variable.
A DLL does not have a main thread of its own. MainThreadID gets initialized to whatever thread is initializing the DLL. So, if your DLL is creating its GUI in a different thread than the one that is initializing your DLL, CheckSynchronize() (and TThread.Synchronize(), and TThread.Queue()) will not work unless you manually update the MainThreadID variable to the ThreadID that is running your GUI. Do that before creating your worker thread, eg:
if IsLibrary then
MainThreadID := GetCurrentThreadID;
Form1 := TForm1.Create(nil);
Or:
procedure TForm1.FormCreate(Sender: TObject);
begin
if IsLibrary then
MainThreadID := GetCurrentThreadID;
end;
Or:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := False;
if IsLibrary then
MainThreadID := GetCurrentThreadID;
APPTHREAD := TAPPTHREAD.Create(false, CallbackProc);
Timer2.Enabled := True;
end;

Delphi download multithreading

My initial question was misleading, I'll try to improve it:
I was able to write a small delphi programme, which is able to do a threaded download by using indy's idHTTP. It consists of 2 files:
the form:
interface
type
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
Button2: TButton;
IdSSLIOHandlerSocketOpenSSL1: TIdSSLIOHandlerSocketOpenSSL;
IdAntiFreeze1: TIdAntiFreeze;
IdHTTP: TIdHTTP;
ProgressBar1: TProgressBar;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
procedure UpdateLabel(BytesDone: Integer);
procedure UpdateProgressBar(AWorkCount: Int64);
procedure InitProgressBar(AWorkCountMax: Int64);
procedure ResetProgressBar;
end;
var
Form1: TForm1;
uStopDownloading: Boolean;
implementation
{$R *.dfm}
uses Unit2;
procedure TForm1.Button1Click(Sender: TObject);
var
HTTPThread: TIdHTTPThread;
begin
HTTPThread := TIdHTTPThread.Create(True);
HTTPThread.Url := 'https://www.bot-factory.de/tmp/lorem7.txt';
HTTPThread.EncodedStr := '';
HTTPThread.Filename := 'C:\test.txt';
HTTPThread.FreeOnTerminate := True;
HTTPThread.Resume;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
idhttp.Disconnect;
end;
procedure TForm1.UpdateLabel(BytesDone: Integer);
begin
Label1.caption := format('%.0n',[extended(BytesDone+0.0)]) +' bytes loaded.';
end;
procedure TForm1.UpdateProgressBar(AWorkCount: Int64);
begin
ProgressBar1.Position := AWorkCount;
end;
procedure TForm1.InitProgressBar(AWorkCountMax: Int64);
begin
Screen.Cursor := crHourGlass;
ProgressBar1.Max := AWorkCountMax;
ProgressBar1.Position := 0;
end;
procedure TForm1.ResetProgressBar;
begin
Screen.Cursor := crDefault;
showmessage('Job is done');
end;
END.
And the Thread-Unit:
interface
uses
Classes, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP, sysutils;
type
TIdHTTPThread = class(TThread)
private
FURL: AnsiString;
FencodedStr: string;
FFilename: AnsiString;
FBytesDone,FProgress,FWorkCountMax: Int64;
IdHTTP: TIdHTTP;
procedure OnWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
procedure OnWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
procedure OnWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
procedure Updatelabel;
procedure UpdateProgressBar;
procedure InitProgressBar;
procedure ResetProgressBar;
procedure Disconnect;
public
Constructor Create(CreateSuspended: Boolean);
Destructor Destroy; override;
property Url: AnsiString read FURL write FUrl;
property encodedstr: String read FencodedStr write FencodedStr;
property Filename: AnsiString read FFilename write FFilename;
protected
procedure Execute; override;
end;
implementation
uses
Unit1; // Formular Unit
constructor TIdHTTPThread.Create(CreateSuspended: Boolean);
begin
inherited Create(Suspended);
IdHTTP := TIdHTTP.Create;
IdHTTP.OnWork := OnWork;
IdHTTP.OnWorkBegin := OnWorkBegin;
IdHTTP.OnWorkEnd := OnWorkEnd;
//IdHTTP.Disconnect := Disconnect;
end;
destructor TIdHTTPThread.Destroy;
begin
IdHTTP.Free;
inherited;
end;
procedure TIdHTTPThread.Execute;
var
DestStream: TFileStream;
begin
DestStream := TFileStream.Create(Filename, fmCreate);
try
IdHTTP.Get(Url, DestStream);
finally
DestStream.Free;
end;
end;
procedure TIdHTTPThread.OnWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
FBytesDone := AWorkCount;
FProgress := AWorkCount;
Synchronize(Updatelabel);
Synchronize(UpdateProgressBar);
end;
procedure TIdHTTPThread.OnWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64);
begin
FWorkCountMax := AWorkCountMax;
Synchronize(InitProgressBar);
end;
procedure TIdHTTPThread.Disconnect;
begin
idhttp.Disconnect;
end;
procedure TIdHTTPThread.OnWorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
Synchronize(ResetProgressBar);
end;
procedure TIdHTTPThread.Updatelabel;
begin
Form1.UpdateLabel(FBytesDone);
end;
procedure TIdHTTPThread.UpdateProgressBar;
begin
Form1.UpdateProgressBar(FProgress);
end;
procedure TIdHTTPThread.InitProgressBar;
begin
Form1.initProgressBar(FWorkCountMax);
end;
procedure TIdHTTPThread.resetProgressBar;
begin
Form1.resetProgressBar;
end;
END.
I have in fact 2 questions:
How can I interrupt the download of a (large) file ? I know that
idhttp.disconnect should do the trick, but I do not know how to use it properly in my thread.
And furthermore: how can I use POST instead of GET?
I need to run this POST in a thread:
{++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//submit_post
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}
Procedure submit_post(url_string,EncodedStr,filename:string);
var
aStream: TMemoryStream;
Params: TStringStream;
begin
astream := TMemoryStream.create;
Params := TStringStream.create('');
Form1.IdHTTP.Request.Clear;
Form1.IdHTTP.HandleRedirects := TRUE;
try
with Form1.IdHTTP do
begin
Params.WriteString(EncodedStr);
Request.ContentType := 'application/x-www-form-urlencoded';
Request.Charset := 'utf-8';
try
Response.KeepAlive := False;
Post(url_string, params, astream);
except
on E: Exception do
begin
exit;
end;
end;
end;
astream.WriteBuffer(#0' ', 1);
astream.Position := 0;
astream.SaveToFile(filename);
finally
astream.Free;
Params.Free;
end;
end;

Delphi multi-threading file write: I/O error 32

I created a class for writing thread-safe log in a text file using CriticalSection.
I am not an expert of CriticalSection and multi-threading programming (...and Delphi), I'm definitely doing something wrong...
unit ErrorLog;
interface
uses
Winapi.Windows, System.SysUtils;
type
TErrorLog = class
private
FTextFile : TextFile;
FLock : TRTLCriticalSection;
public
constructor Create(const aLogFilename:string);
destructor Destroy; override;
procedure Write(const ErrorText: string);
end;
implementation
constructor TErrorLog.Create(const aLogFilename:string);
begin
inherited Create;
InitializeCriticalSection(FLock);
AssignFile(FTextFile, aLogFilename);
if FileExists(aLogFilename) then
Append(FTextFile)
else
Rewrite(FTextFile);
end;
destructor TErrorLog.Destroy;
const
fmTextOpenWrite = 55218;
begin
EnterCriticalSection(FLock);
try
if TTextRec(FTextFile).Mode <> fmTextOpenWrite then
CloseFile(FTextFile);
inherited Destroy;
finally
LeaveCriticalSection(FLock);
DeleteCriticalSection(FLock);
end;
end;
procedure TErrorLog.Write(const ErrorText: string);
begin
EnterCriticalSection(FLock);
try
WriteLn(FTextFile, ErrorText);
finally
LeaveCriticalSection(FLock);
end;
end;
end.
to test the class I created a form with a timer set to 100 milliseconds:
procedure TForm1.Timer1Timer(Sender: TObject);
var
I : integer;
aErrorLog : TErrorLog;
begin
aErrorLog := nil;
for I := 0 to 1000 do begin
try
aErrorLog := TErrorLog.Create(FormatDateTime('ddmmyyyy', Now) + '.txt');
aErrorLog.Write('new line');
finally
if Assigned(aErrorLog) then FreeAndNil(aErrorLog);
end;
end;
end;
the logs are written, but occasionally raise I/O Error 32 exception on CloseFile(FTextFile) (probably because in use in another thread)
where am I doing wrong?
UPDATE:
after reading all the comments and the answers I have totally changed approach. I share my solution.
ThreadUtilities.pas
(* Implemented for Delphi3000.com Articles, 11/01/2004
Chris Baldwin
Director & Chief Architect
Alive Technology Limited
http://www.alivetechnology.com
*)
unit ThreadUtilities;
interface
uses Windows, SysUtils, Classes;
type
EThreadStackFinalized = class(Exception);
TSimpleThread = class;
// Thread Safe Pointer Queue
TThreadQueue = class
private
FFinalized: Boolean;
FIOQueue: THandle;
public
constructor Create;
destructor Destroy; override;
procedure Finalize;
procedure Push(Data: Pointer);
function Pop(var Data: Pointer): Boolean;
property Finalized: Boolean read FFinalized;
end;
TThreadExecuteEvent = procedure (Thread: TThread) of object;
TSimpleThread = class(TThread)
private
FExecuteEvent: TThreadExecuteEvent;
protected
procedure Execute(); override;
public
constructor Create(CreateSuspended: Boolean; ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
end;
TThreadPoolEvent = procedure (Data: Pointer; AThread: TThread) of Object;
TThreadPool = class(TObject)
private
FThreads: TList;
FThreadQueue: TThreadQueue;
FHandlePoolEvent: TThreadPoolEvent;
procedure DoHandleThreadExecute(Thread: TThread);
public
constructor Create( HandlePoolEvent: TThreadPoolEvent; MaxThreads: Integer = 1); virtual;
destructor Destroy; override;
procedure Add(const Data: Pointer);
end;
implementation
{ TThreadQueue }
constructor TThreadQueue.Create;
begin
//-- Create IO Completion Queue
FIOQueue := CreateIOCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
FFinalized := False;
end;
destructor TThreadQueue.Destroy;
begin
//-- Destroy Completion Queue
if (FIOQueue <> 0) then
CloseHandle(FIOQueue);
inherited;
end;
procedure TThreadQueue.Finalize;
begin
//-- Post a finialize pointer on to the queue
PostQueuedCompletionStatus(FIOQueue, 0, 0, Pointer($FFFFFFFF));
FFinalized := True;
end;
(* Pop will return false if the queue is completed *)
function TThreadQueue.Pop(var Data: Pointer): Boolean;
var
A: Cardinal;
OL: POverLapped;
begin
Result := True;
if (not FFinalized) then
//-- Remove/Pop the first pointer from the queue or wait
GetQueuedCompletionStatus(FIOQueue, A, ULONG_PTR(Data), OL, INFINITE);
//-- Check if we have finalized the queue for completion
if FFinalized or (OL = Pointer($FFFFFFFF)) then begin
Data := nil;
Result := False;
Finalize;
end;
end;
procedure TThreadQueue.Push(Data: Pointer);
begin
if FFinalized then
Raise EThreadStackFinalized.Create('Stack is finalized');
//-- Add/Push a pointer on to the end of the queue
PostQueuedCompletionStatus(FIOQueue, 0, Cardinal(Data), nil);
end;
{ TSimpleThread }
constructor TSimpleThread.Create(CreateSuspended: Boolean;
ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
begin
FreeOnTerminate := AFreeOnTerminate;
FExecuteEvent := ExecuteEvent;
inherited Create(CreateSuspended);
end;
procedure TSimpleThread.Execute;
begin
if Assigned(FExecuteEvent) then
FExecuteEvent(Self);
end;
{ TThreadPool }
procedure TThreadPool.Add(const Data: Pointer);
begin
FThreadQueue.Push(Data);
end;
constructor TThreadPool.Create(HandlePoolEvent: TThreadPoolEvent;
MaxThreads: Integer);
begin
FHandlePoolEvent := HandlePoolEvent;
FThreadQueue := TThreadQueue.Create;
FThreads := TList.Create;
while FThreads.Count < MaxThreads do
FThreads.Add(TSimpleThread.Create(False, DoHandleThreadExecute, False));
end;
destructor TThreadPool.Destroy;
var
t: Integer;
begin
FThreadQueue.Finalize;
for t := 0 to FThreads.Count-1 do
TThread(FThreads[t]).Terminate;
while (FThreads.Count > 0) do begin
TThread(FThreads[0]).WaitFor;
TThread(FThreads[0]).Free;
FThreads.Delete(0);
end;
FThreadQueue.Free;
FThreads.Free;
inherited;
end;
procedure TThreadPool.DoHandleThreadExecute(Thread: TThread);
var
Data: Pointer;
begin
while FThreadQueue.Pop(Data) and (not TSimpleThread(Thread).Terminated) do begin
try
FHandlePoolEvent(Data, Thread);
except
end;
end;
end;
end.
ThreadFileLog.pas
(* From: http://delphi.cjcsoft.net/viewthread.php?tid=45763 *)
unit ThreadFileLog;
interface
uses Windows, ThreadUtilities, System.Classes;
type
PLogRequest = ^TLogRequest;
TLogRequest = record
LogText : String;
FileName : String;
end;
TThreadFileLog = class(TObject)
private
FThreadPool: TThreadPool;
procedure HandleLogRequest(Data: Pointer; AThread: TThread);
public
constructor Create();
destructor Destroy; override;
procedure Log(const FileName, LogText: string);
end;
implementation
uses
System.SysUtils;
(* Simple reuse of a logtofile function for example *)
procedure LogToFile(const FileName, LogString: String);
var
F: TextFile;
begin
AssignFile(F, FileName);
if not FileExists(FileName) then
Rewrite(F)
else
Append(F);
try
Writeln(F, LogString);
finally
CloseFile(F);
end;
end;
constructor TThreadFileLog.Create();
begin
FThreadPool := TThreadPool.Create(HandleLogRequest, 1);
end;
destructor TThreadFileLog.Destroy;
begin
FThreadPool.Free;
inherited;
end;
procedure TThreadFileLog.HandleLogRequest(Data: Pointer; AThread: TThread);
var
Request: PLogRequest;
begin
Request := Data;
try
LogToFile(Request^.FileName, Request^.LogText);
finally
Dispose(Request);
end;
end;
procedure TThreadFileLog.Log(const FileName, LogText: string);
var
Request: PLogRequest;
begin
New(Request);
Request^.LogText := LogText;
Request^.FileName := FileName;
FThreadPool.Add(Request);
end;
end.
Basic form example
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls,
Vcl.StdCtrls, ThreadFileLog;
type
TForm1 = class(TForm)
BtnStart: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure BtnStartClick(Sender: TObject);
private
FThreadFileLog : TThreadFileLog;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.BtnStartClick(Sender: TObject);
var
I : integer;
aNow : TDateTime;
begin
aNow := Now;
for I := 0 to 500 do
FThreadFileLog.Log(
FormatDateTime('ddmmyyyyhhnn', aNow) + '.txt',
FormatDateTime('dd-mm-yyyy hh:nn:ss.zzz', aNow) + ': I: ' + I.ToString
);
ShowMessage('logs are performed!');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreadFileLog := TThreadFileLog.Create();
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreadFileLog.Free;
ReportMemoryLeaksOnShutdown := true;
end;
end.
Output log:
30-11-2014 14.01.13.252: I: 0
30-11-2014 14.01.13.252: I: 1
30-11-2014 14.01.13.252: I: 2
30-11-2014 14.01.13.252: I: 3
30-11-2014 14.01.13.252: I: 4
30-11-2014 14.01.13.252: I: 5
30-11-2014 14.01.13.252: I: 6
30-11-2014 14.01.13.252: I: 7
30-11-2014 14.01.13.252: I: 8
30-11-2014 14.01.13.252: I: 9
...
30-11-2014 14.01.13.252: I: 500
Instead of checking TTextRec(FTextFile).Mode <> fmTextOpenWrite you should check whether your file is closed or not, and if it is not closed then you close it.
Try replacing the mentioned check with this code:
if TTextRec(FTextFile).Mode <> fmClosed then
CloseFile(FTextFile);
Edited:
This has nothing to do with antivirus locking the file. This is just a simple mistake in the destructor.
File is already opened in open write mode, original code is closing the file only when it is not in open write mode - so it is never closing the file.
Hope this explains where the mistake has happened.
As for the overall design of the logger's class. This was not the question, questions was simple, and I've provided a simple and working solution.
I think that if Simone would want us to teach him how to design logger class then he would ask for it.
If you want an error log class, where multiple threads can write to a log file, it is correct to protect the writing method with a critical section.
Now, since you will only instantiate one of those error logging objects in your application, there is no need to protect the destructor method with a critical section.
The location of your error log file should reside in the application data folder.
The I/O error 32 is: The process cannot access the file because it is being used by another process.
The reason for this sharing violation could be in your application or an external application.
Writing inside the application directory could trigger some antivirus protection for example. Or your application is holding the file open in several places with different file modes.
Your test is flawed in multiple ways:
Instantiate the error log class once at application start, and destroy it when the application closes.
Write to your error log from different threads, not from multiple iterations within a timer event.
A timer event should only execute a program sequence for a short duration.
A try / finally sequence is structured like this:
anObject := TObject.Create;
try
// Do something with anObject
finally
anObject.Free;
end;

Wait for thread without freezing the application

I'm trying to put an indy TIdHttp in a thread,
I have tried this :
type
TSendThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
public
http : TIdHTTP;
URL : String;
Method : String;
property ReturnValue;
end;
procedure TSendThread.Execute;
begin
form1.Memo1.lines.Add(http.Get(URL));
ReturnValue := 1;
end;
And in the main :
procedure TForm1.Button1Click(Sender: TObject);
var t : TSendThread;
begin
t := TSendThread.Create(true);
t.URL := 'http://www.url.com/';
t.http := http;
t.Start;
showmessage(IntToStr(t.ReturnValue));
end;
My problem here is that the next instruction gets executed(showmessage) without waiting the thread to be done, i tried to use the "WaitFor" but it freezes the application.
Is there any other workaround?
Thank you.
Use the TThread.OnTerminate event to know when the thread has finished:
type
TSendThread = class(TThread)
private
http : TIdHTTP;
Line: string;
procedure AddLine;
protected
procedure Execute; override;
public
constructor Create; reintroduce;
destructor Destroy; override;
URL : String;
Method : String;
property ReturnValue;
end;
constructor TSendThread.Create;
begin
inherited Create(True);
FreeOnTerminate := True;
http := TIdHTTP.Create;
end;
destructor TSendThread.Destroy;
begin
http.Free;
inherited;
end;
procedure TSendThread.Execute;
begin
Line := http.Get(URL);
Synchronize(AddLine);
ReturnValue := 1;
end;
procedure TSendThread.AddLine;
begin
Form1.Memo1.Lines.Add(Line);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
t : TSendThread;
begin
t := TSendThread.Create;
t.URL := 'http://www.url.com/';
t.OnTerminate := ThreadTerminated;
t.Start;
end;
procedure TForm1.ThreadTerminated(Sender: TObject);
begin
ShowMessage(IntToStr(TSendThread(Sender).ReturnValue));
end;
If you want to use a loop to wait for the thread to finish, without blocking the UI, then you can do it like this:
constructor TSendThread.Create;
begin
inherited Create(True);
//FreeOnTerminate := True; // <-- remove this
http := TIdHTTP.Create;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
t : TSendThread;
h : THandle;
begin
t := TSendThread.Create;
try
t.URL := 'http://www.url.com/';
t.Start;
h := t.Handle;
repeat
case MsgWaitForMultipleObjects(1, h, 0, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0: Break;
WAIT_OBJECT_0+1: Application.ProcessMessages;
WAIT_FAILED: RaiseLastOSError;
else
Break;
end;
until False;
ShowMessage(IntToStr(t.ReturnValue));
finally
t.Free;
end;
end;

Resources