Any ideas why I get this warning in Delphi XE:
[DCC Warning] Form1.pas(250): W1036 Variable '$frame' might not have been initialized
procedure TForm1.Action1Execute(Sender: TObject);
var
Thread: TThread;
begin
...
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
procedure ShowLoading(const Show: Boolean);
begin /// <------------- WARNING IS GIVEN FOR THIS LINE (line number 250)
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
...
Button1.Enabled := not Show;
...
end
);
end;
var
i: Integer;
begin
ShowLoading(true);
try
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
... // some UI updates
end
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
... // some UI updates
end
);
finally
ShowLoading(false);
end;
end
).NameThread('Some Thread Name');
Thread.Start;
end;
I do not have anywhere in my code a variable names frame nor $frame. I am even not sure how $frame with $ sign can be a valid identifier.
Smells like compiler magic to me.
PS: Of course the real life xosw is having other than Form1, Button1, Action1 names.
In XE2 this compiler warning is not there.
Anyway, to make your method a bit clearer to the compiler, try this :
procedure TForm1.Action1Execute(Sender : TObject);
var
Thread: TThread;
begin
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
Type
TProcRef = reference to procedure (const Show: Boolean);
var
i: Integer;
ShowLoading : TProcRef;
begin
ShowLoading:=
procedure (const Show: Boolean)
begin
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
// Button1.Enabled := not Show;
end
);
end;
ShowLoading(true);
... // and so on.
Update :
I just checked in XE, this really fixes the compiler warning.
The reference I gave in the comment to the question explains about the warning and frame objects.
So I guess that the XE compiler is getting confused about the frame object ShowLoading is creating. And creating an explicit reference fixed the warning.
Update 2 :
Just showing my test for your reference :
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,Classes, SysUtils;
procedure Action1Execute;
var
Thread: TThread;
begin
Thread := TThread.CreateAnonymousThread(
procedure{Anonymos}()
Type
TProcRef = reference to procedure (const Show: Boolean);
var
i: Integer;
ShowLoading : TProcRef;
begin
ShowLoading:=
procedure (const Show: Boolean)
begin
Thread.Synchronize(Thread,
procedure{Anonymous}()
begin
// Button1.Enabled := not Show;
end
);
end;
ShowLoading(true);
try
Thread.Synchronize( Thread,
procedure{Anonymous}()
begin
// some UI updates
end
);
Thread.Synchronize( Thread,
procedure{Anonymous}()
begin
// some UI updates
end
);
finally
ShowLoading(false);
end;
end
); //.NameThreadForDebugging('Some Thread Name');
Thread.Start;
end;
begin
try
{ TODO -oUser -cConsole Main : Insert code here }
Action1Execute;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Related
Function
function DownloadString(AUrl: string): string;
var
LHttp: TIdHttp;
begin
LHttp := TIdHTTP.Create;
try
LHttp.HandleRedirects := true;
result := LHttp.Get('http://127.0.0.1/a.php?n='+AUrl);
finally
LHttp.Free;
end;
end;
Boot
procedure TForm1.Button1Click(Sender: TObject);
var
LUrlArray: TArray<String>;
begin
LUrlArray := form1.listbox1.Items.ToStringArray;
TThread.CreateAnonymousThread(
procedure
var
LResult: string;
LUrl: string;
begin
for LUrl in LUrlArray do
begin
LResult := DownloadString(LUrl);
TThread.Synchronize(nil,
procedure
begin
if Pos('DENEGADA',LResult)>0 then
begin
Memo1.Lines.Add(LResult);
end
else
begin
Memo1.Lines.Add(LResult + 'DIE');
end;
end
);
end;
end
).Start;
end;
Listbox Lines
http://127.0.0.1/a.php?n=4984
http://127.0.0.1/a.php?n=4986
http://127.0.0.1/a.php?n=4989
in this case only one thread will download all URL's content but I would like to make it creates a thread for each item...
example:
thread1 - check item1 listbox - http://127.0.0.1/a.php?n=4984
thread2 - check next item 4986
thread3 - check next item 4989
how make this? Is there any way to do this ?, I believe that this method will be more effective.
In order to create separate threads, you have to bind the url variable value like this:
procedure TForm1.Button1Click(Sender: TObject);
var
LUrlArray: TArray<String>;
LUrl: String;
function CaptureThreadTask(const s: String) : TProc;
begin
Result :=
procedure
var
LResult : String;
begin
LResult := DownloadString(s);
TThread.Synchronize(nil,
procedure
begin
if Pos('DENEGADA',LResult)>0 then
begin
Memo1.Lines.Add(LResult);
end
else
begin
Memo1.Lines.Add(LResult + 'DIE');
end;
end
);
end;
end;
begin
LUrlArray := form1.listbox1.Items.ToStringArray;
for LUrl in LUrlArray do
// Bind variable LUrl value like this
TThread.CreateAnonymousThread( CaptureThreadTask(LUrl)
).Start;
end;
See Anonymous Methods Variable Binding
You can try using ForEach pattern of omnithreadlibrary :
http://otl.17slon.com/book/chap04.html#highlevel-foreach
http://otl.17slon.com/book/chap04.html#leanpub-auto-iomniblockingcollection
Draft is like that:
TMyForm = class(TForm)
private
DownloadedStrings: iOmniBlockingCollection;
published
DownloadingProgress: TTimer;
MemoSourceURLs: TMemo;
MemoResults: TMemo;
...
published
procedure DownloadingProgressOnTimer( Sender: TObject );
procedure StartButtonClick ( Sender: TObject );
.....
private
property InDownloadProcess: boolean write SetInDownloadProcess;
procedure FlushCollectedData;
end;
procedure TMyForm.StartButtonClick ( Sender: TObject );
begin
DownloadedStrings := TOmniBlockingCollection.Create;
Parallel.ForEach<string>(MemoSourceURLs.Lines)
.NumTasks(10) // we do not want to overload computer by millions of threads when given a long list. We are not "fork bomb"
// .PreserveOrder - usually not a needed option
.Into(DownloadedStrings) // - or you would have to manually seal the container by calling .CompleteAdding AFTER the loop is over in .OnStop option
.NoWait
.Execute(
procedure (const URL: string; var res: TOmniValue)
var Data: string; Success: Boolean;
begin
if my_IsValidUrl(URL) then begin
Success := my_DownloadString( URL, Data);
if Success and my_IsValidData(Data) then begin
if ContainsText(Data, 'denegada') then
Data := Data + ' DIE';
res := Data;
end;
end
);
InDownloadProcess := true;
end;
procedure TMyForm.SetInDownloadProcess(const process: Boolean);
begin
if process then begin
StartButton.Hide;
Prohibit-Form-Closing := true;
MemoSourceURLs.ReadOnly := true;
MemoResults.Clear;
with DownloadingProgress do begin
Interval := 333; // update data in form 3 times per second - often enough
OnTimer := DownloadingProgressOnTimer;
Enabled := True;
end;
end else begin
DownloadingProgress.Enabled := false;
if nil <> DownloadedStrings then
FlushCollectedData; // one last time
Prohibit-Form-Closing := false;
MemoSourceURLs.ReadOnly := false;
StartButton.Show;
end;
end;
procedure TMyForm.FlushCollectedData;
var s: string; value: TOmniValue;
begin
while DownloadedStrings.TryTake(value) do begin
s := value;
MemoResults.Lines.Add(s);
end;
PostMessage( MemoResults.Handle, .... ); // not SendMessage, not Perform
// I do not remember, there was something very easy to make the memo auto-scroll to the last line added
end;
procedure TMyForm.DownloadingProgressOnTimer( Sender: TObject );
begin
if nil = DownloadedStrings then begin
InDownloadProcess := false;
exit;
end;
FlushCollectedData;
if DownloadedStrings.IsCompleted then begin
InDownloadProcess := false; // The ForEach loop is over, everything was downloaded
DownloadedStrings := nil; // free memory
end;
end;
http://docwiki.embarcadero.com/Libraries/XE4/en/System.StrUtils.ContainsText
http://docwiki.embarcadero.com/Libraries/Seattle/en/Vcl.ExtCtrls.TTimer_Properties
PS. note that the online version of the book is old, you perhaps would have to update it to features in the current version of the omnithreadlibrarysources.
PPS: your code has a subtle error:
for LUrl in LUrlArray do
begin
LResult := DownloadString(LUrl);
Given your implementation of DownloadString that means in the case of HTTP error your function would re-return the previous value of LResult again and again and again and.... until the no-error downloading happened.
That is why I changed your function definition to be clear when error happens and no output data is given.
Continue from my other question:
How do I pass and retrieve memory stream from my Application to/from DLL?
I have wrote the DLL using IStream as input/output. The DLL uses IXMLDocument (which at first I thought was related to the follow problem)
Tested it, and it worked well in the main UI. Problems began when I was calling the DLL from a worker thread.
The DLL:
library MyDLL;
uses
Windows,
Variants,
SysUtils,
Classes,
AxCtrls,
ActiveX,
XMLDoc,
XMLIntf;
{$R *.res}
procedure Debug(V: Variant);
begin
OutputDebugString(PChar(VarToStr(V)));
end;
procedure DoProcess(InStream, OutStream: TStream);
var
Doc: IXMLDocument;
begin
InStream.Position := 0;
Doc := TXMLDocument.Create(nil);
Doc.LoadFromStream(InStream);
// plans to do some real work...
OutStream.Position := 0;
Debug('MyDLL DoProcess OK');
end;
function Process(AInStream, AOutStream: IStream): Integer; stdcall;
var
InStream, OutStream: TStream;
begin
try
InStream := TOleStream.Create(AInStream);
try
OutStream := TOleStream.Create(AOutStream);
try
DoProcess(InStream, OutStream);
Result := 0;
finally
OutStream.Free;
end;
finally
InStream.Free;
end;
except
on E: Exception do
begin
Result := -1;
Debug('MyDLL Error: ' + E.Message);
end;
end;
end;
exports
Process;
begin
end.
And my caller application:
implementation
uses
ActiveX,ComObj;
{$R *.dfm}
procedure Debug(V: Variant);
begin
OutputDebugString(PChar(VarToStr(V)));
end;
const
MyDLL = 'MyDLL.dll';
{$DEFINE STATIC_DLL}
{$IFDEF STATIC_DLL}
function Process(AInStream, AOutStream: IStream): Integer; stdcall; external MyDLL;
{$ENDIF}
type
// Dynamic
TDLLProcessProc = function(AInStream, AOutStream: IStream): Integer; stdcall;
function DLLProcess(AInStream, AOutStream: TStream): Integer;
var
InStream, OutStream: IStream;
Module: HMODULE;
DLLProc: TDLLProcessProc;
begin
InStream := TStreamAdapter.Create(AInStream, soReference);
OutStream := TStreamAdapter.Create(AOutStream, soReference);
{$IFDEF STATIC_DLL}
Result := Process(InStream, OutStream); // Static
Exit;
{$ENDIF}
// Dynamic load DLL ...
Module := LoadLibrary(MyDLL);
if Module = 0 then RaiseLastOSError;
try
DLLProc := GetProcAddress(Module, 'Process');
if #DLLProc = nil then RaiseLastOSError;
Result := DLLProc(InStream, OutStream);
finally
FreeLibrary(Module);
end;
end;
type
TDLLThread = class(TThread)
private
FFileName: string;
public
constructor Create(CreateSuspended: Boolean; AFileName: string);
procedure Execute(); override;
end;
constructor TDLLThread.Create(CreateSuspended: Boolean; AFileName: string);
begin
FreeOnTerminate := True;
FFileName := AFileName;
inherited Create(CreateSuspended);
end;
procedure TDLLThread.Execute;
var
InStream, OutStream: TMemoryStream;
RetValue: Integer;
begin
try
//CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
CoInitialize(nil);
try
InStream := TMemoryStream.Create;
try
InStream.LoadFromFile(FFileName);
OutStream := TMemoryStream.Create;
try
RetValue := DLLProcess(InStream, OutStream);
Sleep(0);
Debug('TDLLThread Result=> ' + IntToStr(RetValue));
if RetValue = 0 then
begin
Debug('TDLLThread OK');
end;
finally
OutStream.Free;
end;
finally
InStream.Free;
end;
finally
CoUninitialize;
end;
except
on E: Exception do
begin
Debug('TDLLThread Error: ' + E.Message);
end;
end;
end;
procedure TForm1.Button1Click(Sender: TObject); // Test
var
I: Integer;
begin
for I := 1 to 5 do
TDLLThread.Create(False, '1.xml');
end;
When running some tests I sometimes get Access Violations which even the exceptions blocks can't catch. And the program simply crashes with Runtime error 216 at xxxxxxx or Invalid pointer operation.
I have tried both static and dynamic DLL linking (figured maybe the dynamic linking has race condition in the LoadLibrary/FreeLibrary).
First I thought IXMLDocument was the main issue:
Doc := TXMLDocument.Create(nil);
Doc.LoadFromStream(InStream);
This sometimes randomly failed with no apparent reason with:
Invalid at the top level of the
document.
Or:
A name was started with an invalid character.
I thought maybe it used some shared resources. but even omitting these lines caused AVs!
So the DLL is practically doing nothing special.
I also Don't see anything special which could infect DLLMain.
I have no Idea what is going on... Can someone suggest how to handle this situation? (Can someone reproduce this behavior?)
EDIT: I just wanted to add a related question (with similar IsMultiThread solution):
Delphi DLL - thread safe
And some tips about IsMultiThread:
IsMultiThread Variable
The memory manager in Delphi has optimisations for single threaded use. These are enabled by default. If your code is multi-threaded then this optimisation needs to be disabled. Do that by setting IsMultiThread to True.
In a module that creates a Delphi thread, the framework sets IsMultiThread to True when a thread is created. In your program the threads are created by the host and so nothing in the library sets IsMultiThread to True. So you must do that explicitly in the DLL. In the main section of the library .dpr file write this:
begin
IsMultiThread := True;
end.
So, I've always faced MAJOR headaches when threading in delphi xe4-6, whether it be from threads not executing, exception handling causes app crashes, or simply the on terminate method never getting called. All the workarounds I've been instructed to use have become very tedious with issues still haunting me in XE6. My code generally has looked something like this:
procedure TmLoginForm.LoginClick(Sender: TObject);
var
l:TLoginThread;
begin
SyncTimer.Enabled:=true;
l:=TLoginThread.Create(true);
l.username:=UsernameEdit.Text;
l.password:=PasswordEdit.Text;
l.FreeOnTerminate:=true;
l.Start;
end;
procedure TLoginThread.Execute;
var
Success : Boolean;
Error : String;
begin
inherited;
Success := True;
if login(USERNAME,PASSWORD) then
begin
// do another network call maybe to get dif data.
end else
begin
Success := False;
Error := 'Login Failed. Check User/Pass combo.';
end;
Synchronize(
procedure
if success = true then
begin
DifferentForm.Show;
end else
begin
ShowMessage('Error: '+SLineBreak+Error);
end;
SyncTimer.Enabled := False;
end);
end;
And then I came across this unit from the samples in Delphi and from the forums:
unit AnonThread;
interface
uses
System.Classes, System.SysUtils, System.Generics.Collections;
type
EAnonymousThreadException = class(Exception);
TAnonymousThread<T> = class(TThread)
private
class var
CRunningThreads:TList<TThread>;
private
FThreadFunc: TFunc<T>;
FOnErrorProc: TProc<Exception>;
FOnFinishedProc: TProc<T>;
FResult: T;
FStartSuspended: Boolean;
private
procedure ThreadTerminate(Sender: TObject);
protected
procedure Execute; override;
public
constructor Create(AThreadFunc: TFunc<T>; AOnFinishedProc: TProc<T>;
AOnErrorProc: TProc<Exception>; ACreateSuspended: Boolean = False;
AFreeOnTerminate: Boolean = True);
class constructor Create;
class destructor Destroy;
end;
implementation
{$IFDEF MACOS}
uses
{$IFDEF IOS}
iOSapi.Foundation
{$ELSE}
MacApi.Foundation
{$ENDIF IOS}
;
{$ENDIF MACOS}
{ TAnonymousThread }
class constructor TAnonymousThread<T>.Create;
begin
inherited;
CRunningThreads := TList<TThread>.Create;
end;
class destructor TAnonymousThread<T>.Destroy;
begin
CRunningThreads.Free;
inherited;
end;
constructor TAnonymousThread<T>.Create(AThreadFunc: TFunc<T>; AOnFinishedProc: TProc<T>;
AOnErrorProc: TProc<Exception>; ACreateSuspended: Boolean = False; AFreeOnTerminate: Boolean = True);
begin
FOnFinishedProc := AOnFinishedProc;
FOnErrorProc := AOnErrorProc;
FThreadFunc := AThreadFunc;
OnTerminate := ThreadTerminate;
FreeOnTerminate := AFreeOnTerminate;
FStartSuspended := ACreateSuspended;
//Store a reference to this thread instance so it will play nicely in an ARC
//environment. Failure to do so can result in the TThread.Execute method
//not executing. See http://qc.embarcadero.com/wc/qcmain.aspx?d=113580
CRunningThreads.Add(Self);
inherited Create(ACreateSuspended);
end;
procedure TAnonymousThread<T>.Execute;
{$IFDEF MACOS}
var
lPool: NSAutoreleasePool;
{$ENDIF}
begin
{$IFDEF MACOS}
//Need to create an autorelease pool, otherwise any autorelease objects
//may leak.
//See https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html#//apple_ref/doc/uid/20000047-CJBFBEDI
lPool := TNSAutoreleasePool.Create;
try
{$ENDIF}
FResult := FThreadFunc;
{$IFDEF MACOS}
finally
lPool.drain;
end;
{$ENDIF}
end;
procedure TAnonymousThread<T>.ThreadTerminate(Sender: TObject);
var
lException: Exception;
begin
try
if Assigned(FatalException) and Assigned(FOnErrorProc) then
begin
if FatalException is Exception then
lException := Exception(FatalException)
else
lException := EAnonymousThreadException.Create(FatalException.ClassName);
FOnErrorProc(lException)
end
else if Assigned(FOnFinishedProc) then
FOnFinishedProc(FResult);
finally
CRunningThreads.Remove(Self);
end;
end;
end.
Why is that this anon thread unit above works flawlessly 100% of the time and my code crashes sometimes? For example, I can exec the same thread 6 times in a row, but then maybe on the 7th (or the first for that matter) time it causes the app to crash. No exceptions ever come up when debugging so I dont have a clue where to start fixing the issue. Also, why is it that I need a separate timer that calls "CheckSynchronize" for my code in order to GUI updates to happen but it is not needed when I use the anon thread unit?
Maybe someone can point me in the right direction to ask this question elsewhere if here is not the place. Sorry, I'm diving into documentation already, trying my best to understand.
Here is an example of a thread that may work 20 times in a row, but then randomly cause app to crash
inherited;
try
SQL:= 'Some SQL string';
if GetSQL(SQL,XMLData) then
synchronize(
procedure
var
i:Integer;
begin
try
mTasksForm.TasksListView.BeginUpdate;
if mTasksForm.TasksListView.Items.Count>0 then
mTasksForm.TasksListView.Items.Clear;
XMLDocument := TXMLDocument.Create(nil);
XMLDocument.Active:=True;
XMLDocument.Version:='1.0';
XMLDocument.LoadFromXML(XMLData);
XMLNode:=XMLDocument.DocumentElement.ChildNodes['Record'];
i:=0;
if XMLNode.ChildNodes['ID'].Text <>'' then
while XMLNode <> nil do
begin
LItem := mTasksForm.TasksListView.Items.AddItem;
with LItem do
begin
Text := XMLNode.ChildNodes['LOCATION'].Text;
Detail := XMLNode.ChildNodes['DESC'].Text +
SLineBreak+
'Assigned To: '+XMLNode.ChildNodes['NAME'].Text
tag := StrToInt(XMLNode.ChildNodes['ID'].Text);
color := TRectangle.Create(nil);
with color do
begin
if XMLNode.ChildNodes['STATUS'].Text = STATUS_DONE then
fill.Color := TAlphaColors.Lime
else if XMLNode.ChildNodes['STATUS'].Text = STATUS_OK then
fill.Color := TAlphaColors.Yellow
else
fill.Color := TAlphaColors.Crimson;
stroke.Color := fill.Color;
ButtonText := XMLNode.ChildNodes['STATUS'].Text;
end;
Bitmap := Color.MakeScreenshot;
end;
XMLNode:=XMLNode.NextSibling;
end;
finally
mTasksForm.TasksListView.EndUpdate;
for i := 0 to mTasksForm.TasksListView.Controls.Count-1 do
begin
if mTasksForm.TasksListView.Controls[I].ClassType = TSearchBox then
begin
SearchBox := TSearchBox(mTasksForm.TasksListView.Controls[I]);
Break;
end;
end;
SearchBox.Text:=' ';
SearchBox.text := ''; //have in here because if the searchbox has text, when attempting to add items then app crashes
end;
end)
else
error := 'Please check internet connection.';
finally
synchronize(
procedure
begin
if error <> '' then
ShowMessage('Erorr: '+error);
mTasksForm.Spinner.Visible:=false;
mTasksForm.SyncTimer.Enabled:=false;
end);
end;
end;
here is the GETSQL method
function GetSQL(SQL:String;var XMLData:String):Boolean;
var
PostResult,
ReturnCode : String;
PostData : TStringList;
IdHTTP : TIdHTTP;
XMLDocument : IXMLDocument;
XMLNode : IXMLNode;
Test : String;
begin
Result:=False;
XMLData:='';
XMLDocument:=TXMLDocument.Create(nil);
IdHTTP:=TIdHTTP.Create(nil);
PostData:=TStringList.Create;
PostData.Add('session='+SessionID);
PostData.Add('database='+Encode(DATABASE,''));
PostData.Add('sql='+Encode(SQL,''));
IdHTTP.Request.ContentEncoding:='UTF-8';
IdHTTP.Request.ContentType:='application/x-www-form-urlencoded';
IdHTTP.ConnectTimeout:=100000;
IdHTTP.ReadTimeout:=1000000;
try
PostResult:=IdHTTP.Post(SERVER_URL+GET_METHOD,PostData);
XMLDocument.Active:=True;
XMLDocument.Version:='1.0';
test := Decode(PostResult,'');
XMLDocument.LoadFromXML(Decode(PostResult,''));
XMLNode:=XMLDocument.DocumentElement;
try
ReturnCode:=XMLNode.ChildNodes['status'].Text;
except
ReturnCode:='200';
end;
if ReturnCode='' then begin
ReturnCode:='200';
end;
if ReturnCode='200' then begin
Result:=True;
XMLData:=Decode(PostResult,'');
end;
except
on E: Exception do begin
result:=false;
end;
end;
PostData.Free;
IdHTTP.Free;
end;
I have the following problem/question.
I have a unit named "myGlobalFunctions.pas".
Inside this unit I have implemented multiple procedures/functions that are used by several projects.
project 1 use this unit
project 3 use this unit
project 6 use this unit
etc
inside "project 1" there is a thread that use functions inside the "global function" unit.
inside project 3 there is no thread but the functions are used.
so far this thread (project1) provide almost no update of the application interface and the update was made AFTER or BEFORE calling a function from "myGlobalFunctions.pas"
like "before start function1"
... the calling
"after function1".
this way I can know what the program is doing.
However now I want to implement inside the "function1" update of the application interface (with synchronize).
I want to reflect in the application interface "processing step1 ... xx records". (there is a while loop there for a dataset).
using Synchronize for "project1" and with normal label1.caption = 'message'; application.process messages for any other project.
is it possible?
how can I do such a thing.
can be Thread Safe ?
tks a lot
Razvan
here is some code to understand better
THREAD UNIT
procedure TThreadSyncronizeProcess.SignalStart;
begin
frmMain.sbMain.Panels[2].Text := 'Syncronizare in desfasurare...'; -- exist all the time
if Assigned(frmSyncronize) then begin -- check if exist this
frmSyncronize.logMain.WriteFeedBackMessage('Pornire syncronizare...', '', EVENTLOG_INFORMATION_TYPE, True);
end;
end;
procedure TThreadSyncronizeProcess.Execute;
var ..... declarations
begin
Synchronize(SignalStart); -- this is normal call within thread update interface
try
try
workSession := TIB_Session.Create(nil);
workDatabase := TIB_Database.Create(workSession);
... creating more components and setup them ...
uSyncronizareFunctions.SetupDatabase(workDatabase, workSession, transactionWrite, transactionRead);
uSyncronizareFunctions.SetupDataSnapConnection(workConnectionRead, providerRead);
if Assigned(frmSyncronize) then begin
uSyncronizareFunctions.SetupFeedBack(frmSyncronize.logMain);
end;
try
Synchronize(SignalMessage);
// this next function is from the "global unit"
isAllOk := uSyncronizareFunctions.ImportOperatoriAutorizati(workImage, workLabelProgress, True);
isAllOk := isAllOk and uSyncronizareFunctions.ImportJudete;
isAllOk := isAllOk and uSyncronizareFunctions.ImportLocalitati;
isAllOk := isAllOk and uSyncronizareFunctions.ImportUM;
isAllOk := isAllOk and uSyncronizareFunctions.ImportFurnizori;
isAllOk := isAllOk and uSyncronizareFunctions.ImportClasificari;
except
on e : Exception do begin
raise Exception.Create(dmMain.GetDataSnapExceptionMessage(e.Message));
end;
end;
except
on e : Exception do begin
baseMessage := e.Message;
Synchronize(SignalMessage);
end;
end;
finally
workDatabase.ForceDisconnect;
FreeAndNil(transactionRead);
... etc
end;
Synchronize(SignalFinish);
end;
global function unit
unit uSyncronizareFunctions;
function ImportOperatoriAutorizati(imgDone : TImage; labelProgress : TLabel; isThread : Boolean) : Boolean;
var workQuery : TIB_Query;
serverData : TClientDataSet;
begin
Result := True;
try
... create all that we need
serverData.Close;
serverData.CommandText := 'SELECT * FROM OPERATORI_AUTORIZATI WHERE REC_VERSION > :ARECVERSION ORDER BY REC_VERSION, ID';
serverData.Params.Clear;
serverData.Params.CreateParam(ftInteger, 'ARECVERSION', ptInput);
serverData.Params.ParamByName('ARECVERSION').AsInteger := lastVersion;
serverData.Active := True;
...... I want here to signal start
while not serverData.Eof do begin
try
globalInsert_Tran.StartTransaction;
workQuery.Close;
workQuery.ParamByName('AIDGLOBAL').AsString := serverData.FieldByName('IDGLOBAL').AsString;
workQuery.Open;
if workQuery.IsEmpty then begin
workQuery.Insert;
workQuery.FieldByName('IDGLOBAL').AsString := serverData.FieldByName('IDGLOBAL').AsString;
end else begin
workQuery.Edit;
end;
workQuery.FieldByName('NUME').AsString := serverData.FieldByName('NUME').AsString;
workQuery.FieldByName('COD_AUTORIZARE').AsString := serverData.FieldByName('COD_AUTORIZARE').AsString;
workQuery.FieldByName('OTHER_INFO').AsString := serverData.FieldByName('OTHER_INFO').AsString;
workQuery.FieldByName('DATASTERGERE').AsVariant := GetValueDate(serverData.FieldByName('DATASTERGERE').AsDateTime);
workQuery.FieldByName('REC_VERSION').AsInteger := serverData.FieldByName('REC_VERSION').AsInteger;
workQuery.Post;
MarkRecordAsDirtyFalse(workQuery);
globalInsert_Tran.Commit;
...... I want here to signal progress and to see in the application interface "processing record xx/100" or any other message
except
on e : Exception do begin
Result := False;
globalInsert_Tran.Rollback;
end;
end;
serverData.Next;
end;
finally
FreeAndNil(serverData);
FreeAndNil(workQuery);
end;
end;
It looks like you would like your global function to execute a callback. You might try an approach like this:
unit MyGlobalMethods;
interface
uses
System.SysUtils;
type
// define a method signature for your callback
TSomeCallback = procedure(progress : integer) of object;
// add a callback argument to your function (initializing to nil will make
// the parameter optional and will not break your previous implementations)
function GlobalFunction(arg1 : integer;
AMethodCallback : TSomeCallback = nil) : boolean;
implementation
function GlobalFunction(arg1 : integer;
AMethodCallback : TSomeCallback) : boolean;
var i : integer;
begin
for i := 0 to arg1 do begin
sleep(10); // Do some work
// report progress by executing the callback method
// only do this if a method has been passed as argument
if (i mod 100 = 0) and (Assigned(AMethodCallback)) then AMethodCallback(i);
end;
result := true;
end;
end.
Adding a method callback as an argument allows you to pass in any function you like to have the method execute. For example :
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
private
procedure UpdateProgress(progress : integer);
end;
TSomeThread = class(TThread)
private
FProgressCallback : TSomeCallback;
FProgress : integer;
procedure SynchronizeCallback(progress : integer);
procedure DoCallback;
public
procedure Execute; override;
property OnFunctionProgress : TSomeCallback
read FProgressCallback write FProgressCallback;
end;
implement as :
procedure TSomeThread.Execute;
begin
GlobalFunction(1000, SynchronizeCallback);
end;
procedure TSomeThread.SynchronizeCallback(progress: Integer);
begin
FProgress := progress;
Synchronize(DoCallback);
end;
procedure TSomeThread.DoCallback;
begin
if Assigned(FProgressCallback) then FProgressCallback(FProgress);
end;
You haven't told us what version of Delphi you are using. If you are using D2009 or newer you can bundle the above two calls into one using anonymous methods (and get rid of FProgress) :
procedure TSomeThread.SynchronizeCallback(progress: Integer);
begin
Synchronize(procedure
begin
if Assigned(FProgressCallback) then FProgressCallback(progress);
end;);
end;
Where in your form you would do :
procedure TForm1.UpdateProgress(progress: Integer);
begin
label1.Caption := IntToStr(progress);
end;
procedure TForm1.Button1Click(Sender: TObject);
var someThread : TSomeThread;
begin
someThread := TSomeThread.Create(true);
someThread.FreeOnTerminate := true;
someThread.OnFunctionProgress := UpdateProgress;
someThread.Start;
end;
This nicely separates the responsibilities. The main form passes an update method to the thread (a method, in this case, to update a label). The thread is responsible for synchronizing the call and the global function, therefore, does not need to care whether or not the callback it is executing originates from the main thread or from any other thread. The thread knows it needs to synchronize the method so it should take that responsibility.
I'm writing a thread which writes event logs. When the application is closed (gracefully), I need to make sure this thread finishes its job saving the logs before it's free'd. If I call Free directly to the thread, it shouldn't immediately be destroyed, it should wait until the thread is done and there's no more work left to do.
Here is how I have my thread's execution laid out:
procedure TEventLogger.Execute;
var
L: TList;
E: PEventLog; //Custom record pointer
begin
while not Terminated do begin //Repeat continuously until terminated
try
E:= nil;
L:= LockList; //Acquire locked queue of logs to be written
try
if L.Count > 0 then begin //Check if any logs exist in queue
E:= PEventLog(L[0]); //Get next log from queue
L.Delete(0); //Remove log from queue
end;
finally
UnlockList;
end;
if E <> nil then begin
WriteEventLog(E); //Actual call to save log
end;
except
//Handle exception...
end;
Sleep(1);
end;
end;
And here's the destructor...
destructor TEventLogger.Destroy;
begin
ClearQueue; //I'm sure this should be removed
FQueue.Free;
DeleteCriticalSection(FListLock);
inherited;
end;
Now I already know that at the time when Free is called, I should raise a flag making it impossible to add any more logs to the queue - it just needs to finish what's already there. My issue is that I know the above code will forcefully be cut off when the thread is free'd.
How should I make this thread finish its work when Free has been called? Or if that's not possible, how in general should this thread be structured for this to happen?
If I call Free directly to the thread, it shouldn't immediately be destroyed, it should wait until the thread is done and there's no more work left to do.
I think you have a slight mis-understanding of what happens when you destroy a thread. When you call Free on a TThread, the following happens in the destructor:
Terminate is called.
WaitFor is called.
The remainder of the thread's destructor then runs.
In other words, calling Free already does what you ask for, namely notifying the thread method that it needs to terminate, and then waiting for it to do so.
Since you are in control of the thread's Execute method, you can do as much or as little work there once you detect that the Terminated flag has been set. As Remy suggests, you could override DoTerminate and do your last pieces of work there.
For what it is worth, this is a poor way to implement a queue. That call to Sleep(1) jumps right out at me. What you need is a blocking queue. You empty the queue and then wait on an event. When the producer adds to the queue the event is signaled so that your thread can wake up.
This is my take on how to write a consumer thread. The first piece of the jigsaw is a blocking queue. Mine looks like this:
unit BlockingQueue;
interface
uses
Windows, SyncObjs, Generics.Collections;
type
TBlockingQueue<T> = class
//see Duffy, Concurrent Programming on Windows, pp248
private
FCapacity: Integer;
FQueue: TQueue<T>;
FLock: TCriticalSection;
FNotEmpty: TEvent;
function DoEnqueue(const Value: T; IgnoreCapacity: Boolean): Boolean;
public
constructor Create(Capacity: Integer=-1);//default to unbounded
destructor Destroy; override;
function Enqueue(const Value: T): Boolean;
procedure ForceEnqueue(const Value: T);
function Dequeue: T;
end;
implementation
{ TBlockingQueue<T> }
constructor TBlockingQueue<T>.Create(Capacity: Integer);
begin
inherited Create;
FCapacity := Capacity;
FQueue := TQueue<T>.Create;
FLock := TCriticalSection.Create;
FNotEmpty := TEvent.Create(nil, True, False, '');
end;
destructor TBlockingQueue<T>.Destroy;
begin
FNotEmpty.Free;
FLock.Free;
FQueue.Free;
inherited;
end;
function TBlockingQueue<T>.DoEnqueue(const Value: T; IgnoreCapacity: Boolean): Boolean;
var
WasEmpty: Boolean;
begin
FLock.Acquire;
Try
Result := IgnoreCapacity or (FCapacity=-1) or (FQueue.Count<FCapacity);
if Result then begin
WasEmpty := FQueue.Count=0;
FQueue.Enqueue(Value);
if WasEmpty then begin
FNotEmpty.SetEvent;
end;
end;
Finally
FLock.Release;
End;
end;
function TBlockingQueue<T>.Enqueue(const Value: T): Boolean;
begin
Result := DoEnqueue(Value, False);
end;
procedure TBlockingQueue<T>.ForceEnqueue(const Value: T);
begin
DoEnqueue(Value, True);
end;
function TBlockingQueue<T>.Dequeue: T;
begin
FLock.Acquire;
Try
while FQueue.Count=0 do begin
FLock.Release;
Try
FNotEmpty.WaitFor;
Finally
FLock.Acquire;
End;
end;
Result := FQueue.Dequeue;
if FQueue.Count=0 then begin
FNotEmpty.ResetEvent;
end;
Finally
FLock.Release;
End;
end;
end.
It is completely threadsafe. Any thread can enqueue. Any thread can dequeue. The dequeue function will block if the queue is empty. The queue can be operated in either bounded or unbounded modes.
Next up we need a thread that works with such a queue. The thread simply pulls jobs off the queue until it is told to terminate. My consumer thread looks like this:
unit ConsumerThread;
interface
uses
SysUtils, Classes, BlockingQueue;
type
TConsumerThread = class(TThread)
private
FQueue: TBlockingQueue<TProc>;
FQueueFinished: Boolean;
procedure SetQueueFinished;
protected
procedure TerminatedSet; override;
procedure Execute; override;
public
constructor Create(Queue: TBlockingQueue<TProc>);
end;
implementation
{ TConsumerThread }
constructor TConsumerThread.Create(Queue: TBlockingQueue<TProc>);
begin
inherited Create(False);
FQueue := Queue;
end;
procedure TConsumerThread.SetQueueFinished;
begin
FQueueFinished := True;
end;
procedure TConsumerThread.TerminatedSet;
begin
inherited;
//ensure that, if the queue is empty, we wake up the thread so that it can quit
FQueue.ForceEnqueue(SetQueueFinished);
end;
procedure TConsumerThread.Execute;
var
Proc: TProc;
begin
while not FQueueFinished do begin
Proc := FQueue.Dequeue();
Proc();
Proc := nil;//clear Proc immediately, rather than waiting for Dequeue to return since it blocks
end;
end;
end.
This has the very property that you are looking for. Namely that when the thread is destroyed, it will process all pending tasks before completing the destructor.
To see it in action, here's a short demonstration program:
unit Main;
interface
uses
Windows, SysUtils, Classes, Controls, Forms, StdCtrls,
BlockingQueue, ConsumerThread;
type
TMainForm = class(TForm)
Memo1: TMemo;
TaskCount: TEdit;
Start: TButton;
Stop: TButton;
procedure StartClick(Sender: TObject);
procedure StopClick(Sender: TObject);
private
FQueue: TBlockingQueue<TProc>;
FThread: TConsumerThread;
procedure Proc;
procedure Output(const Msg: string);
end;
implementation
{$R *.dfm}
procedure TMainForm.Output(const Msg: string);
begin
TThread.Synchronize(FThread,
procedure
begin
Memo1.Lines.Add(Msg);
end
);
end;
procedure TMainForm.Proc;
begin
Output(Format('Consumer thread ID: %d', [GetCurrentThreadId]));
Sleep(1000);
end;
procedure TMainForm.StartClick(Sender: TObject);
var
i: Integer;
begin
Memo1.Clear;
Output(Format('Main thread ID: %d', [GetCurrentThreadId]));
FQueue := TBlockingQueue<TProc>.Create;
FThread := TConsumerThread.Create(FQueue);
for i := 1 to StrToInt(TaskCount.Text) do
FQueue.Enqueue(Proc);
end;
procedure TMainForm.StopClick(Sender: TObject);
begin
Output('Stop clicked, calling thread destructor');
FreeAndNil(FThread);
Output('Thread destroyed');
FreeAndNil(FQueue);
end;
end.
object MainForm: TMainForm
Caption = 'MainForm'
ClientHeight = 560
ClientWidth = 904
object Memo1: TMemo
Left = 0
Top = 96
Width = 904
Height = 464
Align = alBottom
end
object TaskCount: TEdit
Left = 8
Top = 8
Width = 121
Height = 21
Text = '10'
end
object Start: TButton
Left = 8
Top = 48
Width = 89
Height = 23
Caption = 'Start'
OnClick = StartClick
end
object Stop: TButton
Left = 120
Top = 48
Width = 75
Height = 23
Caption = 'Stop'
OnClick = StopClick
end
end
Here is a "lazy" EventLogger thread which will save all events in the queue.
unit EventLogger;
interface
uses
Classes, SyncObjs, Contnrs;
type
TEventItem = class
TimeStamp : TDateTime;
Info : string;
end;
TEventLogger = class( TThread )
private
FStream : TStream;
FEvent : TEvent;
FQueue : TThreadList;
protected
procedure TerminatedSet; override;
procedure Execute; override;
procedure WriteEvents;
function GetFirstItem( out AItem : TEventItem ) : Boolean;
public
constructor Create; overload;
constructor Create( CreateSuspended : Boolean ); overload;
destructor Destroy; override;
procedure LogEvent( const AInfo : string );
end;
implementation
uses
Windows, SysUtils;
{ TEventLogger }
constructor TEventLogger.Create( CreateSuspended : Boolean );
begin
FEvent := TEvent.Create;
FQueue := TThreadList.Create;
inherited;
end;
constructor TEventLogger.Create;
begin
Create( False );
end;
destructor TEventLogger.Destroy;
begin
// first the inherited part
inherited;
// now freeing the internal instances
FStream.Free;
FQueue.Free;
FEvent.Free;
end;
procedure TEventLogger.Execute;
var
LFinished : Boolean;
begin
inherited;
LFinished := False;
while not LFinished do
begin
// waiting for event with 20 seconds timeout
// maybe terminated or full queue
WaitForSingleObject( FEvent.Handle, 20000 );
// thread will finished if terminated
LFinished := Terminated;
// write all events from queue
WriteEvents;
// if the thread gets terminated while writing
// it will be still not finished ... and therefor one more loop
end;
end;
function TEventLogger.GetFirstItem( out AItem : TEventItem ) : Boolean;
var
LList : TList;
begin
LList := FQueue.LockList;
try
if LList.Count > 0
then
begin
AItem := TEventItem( LList[0] );
LList.Delete( 0 );
Result := True;
end
else
Result := False;
finally
FQueue.UnlockList;
end;
end;
procedure TEventLogger.LogEvent( const AInfo : string );
var
LList : TList;
LItem : TEventItem;
begin
if Terminated
then
Exit;
LItem := TEventItem.Create;
LItem.TimeStamp := now;
LItem.Info := AInfo;
LList := FQueue.LockList;
try
LList.Add( LItem );
// if the queue is "full" we will set the event
if LList.Count > 50
then
FEvent.SetEvent;
finally
FQueue.UnlockList;
end;
end;
procedure TEventLogger.TerminatedSet;
begin
// this is called if the thread is terminated
inherited;
FEvent.SetEvent;
end;
procedure TEventLogger.WriteEvents;
var
LItem : TEventItem;
LStream : TStream;
begin
// retrieve the first event in list
while GetFirstItem( LItem ) do
try
// writing the event to a file
if not Assigned( FStream )
then
FStream := TFileStream.Create( ChangeFileExt( ParamStr( 0 ), '.log' ), fmCreate or fmShareDenyWrite );
// just a simple log row
LStream :=
TStringStream.Create(
Format(
'[%s] %s : %s',
// when it is written to file
[FormatDateTime( 'dd.mm.yyyy hh:nn:ss.zzz', now ),
// when did it happend
FormatDateTime( 'dd.mm.yyyy hh:nn:ss.zzz', LItem.TimeStamp ),
// whats about
LItem.Info] ) + sLineBreak,
TEncoding.UTF8 );
try
LStream.Seek( 0, soFromBeginning );
FStream.CopyFrom( LStream, LStream.Size );
finally
LStream.Free;
end;
finally
LItem.Free;
end;
end;
end.
Modifying your code, I would suggest checking the last queue count in the while as well, notice variable LastCount I introduced here:
procedure TEventLogger.Execute;
var
L: TList;
E: PEventLog; //Custom record pointer
LastCount: integer;
begin
LastCount:=0;//counter warning
while not (Terminated and (LastCount=0)) do begin //Repeat continuously until terminated
try
E:= nil;
L:= LockList; //Acquire locked queue of logs to be written
try
LastCount:=L.Count;
if LastCount > 0 then begin //Check if any logs exist in queue
E:= PEventLog(L[0]); //Get next log from queue
L.Delete(0); //Remove log from queue
end;
finally
UnlockList;
end;
if E <> nil then begin
WriteEventLog(E); //Actual call to save log
end;
except
//Handle exception...
end;
Sleep(1);
end;
end;