I have built the backend (with WebBroker) and it has some APIs. So in the client I am going to use the REST components to get the json and parse it. This is an example:
procedure TForm1.ButtonCreateClick(Sender: TObject);
begin
//rreqTodoCreate is a TRESTRequest component!
rreqTodoCreate.Params[0].Value := EditTitle.Text;
rreqTodoCreate.Params[1].Value := EditCategory.Text;
rreqTodoCreate.ExecuteAsync(procedure
begin
ResponseEdit.Text := rrespToDo.Content;
end);
end;
I am using the ExecuteAsync as the docwiki suggests also because I am on mobile and I don't want the UI to freeze! I have read that ExecuteAsync runs on a separate thread and so I have a doubt.
Is the code I have written thread-safe? Or in other words: should I use Queue or Synchronize when I update the text of a component in the main form?
Per the TRESTRequest.ExecuteAsync() documentation:
Parameters
This method defines the following parameters:
ACompletionHandler -- Specifies an anonymous method to run after completing the request execution.
ASynchronized -- When True, specifies that the method set in ACompletionHandler runs in the main thread context. When False, ACompletionHandler runs in the execution thread context.
AFreeThread - When True, the execution thread is freed after completing the request execution.
The ASynchronized parameter is True by default:
function ExecuteAsync(
ACompletionHandler: TCompletionHandler = nil;
ASynchronized: Boolean = True; // <--
AFreeThread: Boolean = True;
ACompletionHandlerWithError: TCompletionHandlerWithError = nil): TRESTExecutionThread;
So, the code you have shown is perfectly fine as-is, the assignment of the ResponseEdit.Text is thread-safe.
Yes, I also think that this is better:
rreqTodoCreate.ExecuteAsync(procedure
begin
TThread.Queue(procedure
begin
ResponseEdit.BeginUpdate;
ResponseEdit.Text := rrespToDo.Content;
ResponseEdit.EndUpdate;
end;
end);
Explanation:
TThread.Queue executes the anonymous method in the main thread
BeginUpdate and EndUpdate freeze the UI and speed up the update of form
Related
Under firemonkey, When i want to execute some code after the current "cycle", I do like this :
TThread.createAnonymousThread(
procedure
begin
TThread.queue(nil,
procedure
begin
domycode
end);
end).start;
because if we are in the mainThread, then TThread.queue will execute the code immediatly. I m curious if their is not another way to do this than using a thread ?
In 10.2 Tokyo, a new TThread.ForceQueue() method was added to address RSP-15427 (Add an option to let TThread.Queue() run asynchronously when called by the main UI thread):
TThread.ForceQueue(nil,
procedure
begin
domycode
end
);
No thread is needed.
Prior to Tokyo, you would have to re-write the code if you don't want to use an anonymous thread to call TThread.Queue(). For instance, you could post yourself a delayed message with PostMessage() or PostThreadMessage(), and then do the work in the message handler. Or use the TApplication(Events).OnIdle event, like GolezTrol suggested.
I am using CreateAnonymousThread for a worker task, and when I started with it I used Synchronize within the entire declaration as per documented examples, e.g:
procedure Txxx.RunWorker;
begin
FExecutionThread := TThread.CreateAnonymousThread(procedure ()
begin
TThread.Synchronize (TThread.CurrentThread,
procedure ()
begin
// Here before worker stuff
NotifyBeforeWorkerStuff;
end);
// Do worker stuff
TThread.Synchronize (TThread.CurrentThread,
procedure ()
begin
// Here after worker stuff
NotifyAfterWorkerStuff;
end);
end);
FExecutionThread.Start;
end;
end;
As you see, from within this thread I launch event notifications to various parts of my app including VCL forms (NotifyBeforeWorkerStuff etc).
Later, I saw that I could move Synchronize() more locally to each VCL form close to the point that actually required it for updating (non-safe) VCL controls:
procedure TSomeVCLForm.ReceiveNotification;
begin
TThread.Synchronize (TThread.CurrentThread,
procedure ()
begin
Label1.Caption := GetSomeStringFunction;
end);
end;
The worker thread then becomes simpler as long as I live with notifications being from either main or worker threads:
procedure Txxx.RunWorker;
begin
FExecutionThread := TThread.CreateAnonymousThread(procedure ()
begin
NotifyBeforeWorkerStuff;
// Do worker stuff
NotifyAfterWorkerStuff;
end);
FExecutionThread.Start;
end;
I have several questions about whether this is correct:
My notifications may be from the worker thread but also from the main thread (e.g derived from a button press). So, when 'ReceiveNotification' on a VCL form is called from the main thread, is it allowed to call TThread.Synchronize as above? The the XE8 docs imply not, but checking System.Classes this looks ok and it works fine.
Within 'ReceiveNotification' when Label1.Caption fetches the string from GetSomeStringFunction, is it correct that there is absolutely no need for locking within that function even when the call is from a worker thread?
Thanks for any advice.
The documentation says:
Warning: Do not call Synchronize from within the main thread. This can cause an infinite loop.
I think that documentation is simply wrong. The implementation in XE8 checks whether or not the current thread is the main thread. If it is then the method is executed directly.
No locking is required in ReceiveNotification because the call to GetSomeStringFunction is always performed on the main thread.
The app seems to freeze sometimes when we try to pass a lot of imported files at once, which is done a each call of the function below for each file so the proposed solution is to add a sleep, but I can't seem to find proper documentation or explaining on how to handle it, or if I can even pass it as a parameter in a function.
This is the call for the proc
OpenQuery(FOrderToImportQuery.Database,FOrderToImportQuery);
My suggested idea if I can pass Sleep as Param
OpenQuery(FOrderToImportQuery.Database,FOrderToImportQuery, Sleep(200));
This is the function itself minus the sleep
procedure OpenQuery(aDatabase : TIBDatabase; aQuery : TIBQuery);
begin
if aDatabase.Connected = false then
databaseConnect(aDatabase);
if aDatabase.connected then
begin
try
aQuery.Open;
except
//try
aDatabase.ForceClose;
aDatabase.Open;
aQuery.Open;
{
except
on e: exception do
begin
Log('Error opening query : '+e.Message);
end;
end;
}
end;
end;
end;
The idea is I want the call to wait so it can complete properly before being called again. Would it be just fine to put Sleep at the end of the function itself?(Before the last END)
Or would passing it as a parameter in the call of the function be best? And if is so, how is this achieved... I can't find any doc on this particular circumstance.
The idea is I want the call to wait so it can complete properly before being called again.
Then the idea of using Sleep() is completely misconceived.
If, in a single thread, you call procedures A, B and C, as in
A;
B;
C;
then execution in the thread will only ever proceed to B after the call to A returns. Adding a Sleep() in either of them or in between them will only delay things: if there is a "log-jam" in A, adding a call to Sleep() in or after it will make no difference whatsoever. The fact that A, B and C all call your OpenQuery makes no difference either.
This is true even if A runs an asynchronous query, because the whole point of a call to an asynchronous query is that the call returns before the query completes - an asynchronous query spawns its own background thread in which the query actually executes, then typically passes the results back to the VCL thread via a call to Synchronize().
You have had comments suggesting that you put your query in a separate worker thread (separate from the VCL thread, that is). That's fine for stopping the VCL thread seizing up while waiting for the query(s) to complete, but including calls to Sleep() in the worker thread won't help there either.
So, the real answer to your q is for you to investigate and solve why a single call to OpenQuery causes the program to hang. But that's not what you've asked ...
First of all, let me say that I'm assuming your code is as optimized as it can be, and the time it takes to complete is inherently long. If you believe this might not be the case, you should open a new question with the details of your queries so we can help you on this.
Sleeping your main thread is definitely not the answer
The Sleep function will actually suspend the main thread for the amount of milliseconds specified. So, you will actually just be freezing your gui even more than now.
Worker thread
Creating a worker thread to handle the long-running work is probably your best bet to keep your program responsive while it's doing all the dirty work.
You'll have to take some precautions, though, because you probably don't want the user to be using the program while it's running the worker thread. For example, you don't want the user to click the start button again; or close the application, etc. But if these precautions are something like freezing the main thread, then you better just freeze it with the long-running work, anyway.
Maybe you will want a cancel button somewhere, if this is a process that can be interrupted in the middle (proper control of database transactions could provide this option safely).
Your worker thread could be something along these lines:
type
TWorkerThread = class(TThread)
private
{ Private declarations }
FDatabase: TIBDatabase;
FListQueries: TStringList;
protected
procedure Execute; override;
public
constructor Create(aDatabase: TIBDatabase; ListQueries: TStringList; CreateSuspended: Boolean);
destructor Destroy; override;
end;
implementation
{ TWorkerThread }
constructor TWorkerThread.Create(aDatabase: TIBDatabase; ListQueries: TStringList; CreateSuspended: Boolean);
begin
FListQueries.Create;
FListQueries.Assign(ListQueries);
FDatabase := aDatabase;
inherited Create(CreateSuspended);
end;
destructor TWorkerThread.Destroy;
begin
FListQueries.Free;
inherited;
end;
procedure TWorkerThread.Execute;
var i: Integer;
ibQuery: TIBQuery;
begin
{ Place thread code here }
ibQuery := TIBQuery.Create(aDatabase);
try
for i := 0 to FListQueries.Count - 1 do begin
if Terminated then
Exit;
ibQuery.SQL.Clear;
ibQuery.SQL.Add(FListQueries[i]);
OpenQuery(FDatabase, ibQuery);
end;
finally
ibQuery.Free;
end;
end;
PS: I'm sorry if there are compilation errors or if code for TIBDatabase/TIBQuery is wrong, I don't use any of these.
PPS: There is probably a problem with this code, though: I believe that the TIBConnection is very likely to not be thread-safe (I believe the client library itself is not). So you actually should create one connection just for use within the worker thread, rather than just use the same from main thread. I'll leave this correction for you, though. ;)
I came across this piece of Delphi code while searching for background methods to execute tasks in firemonkey.
TThread.CreateAnonymousThread(
procedure()
begin
Sleep(10000);
TThread.Synchronize(TThread.CurrentThread,
procedure
begin
Button2.Text := 'OK';
end);
end).Start;
Is TThread.Synchronize at this case really necessary?
TButton.Text changes a property of a Window object, which is inherently non-thread-safe, and is only to be accessed directly from the thread which created it OR via message sends/posts.
What TThread.Synchronize does is - it wraps the procedure together with a waitable object, places this to a queue, and waits on the handle - it may post a message to the main thread to wake it up.
If the code behind TButton.Text was implemented via posted message - and is is not - it would be safe to call from other threads, but it would not take effect immediately.
Long story short - you definitely have to call this via Synchronize, for good reasons.
Actually i am using the AsyncCalls library to execute an Query asynchronously in this way.
while AsyncMultiSync([RunQuery], True, 10) = WAIT_TIMEOUT do
begin
FrmProgress.refresh; //Update the ellapsed time in a popup form
Application.ProcessMessages;
end;
and everything works ok.
Now i want to do the same for load the query in a grid.
so i tried this
while LocalAsyncVclCall(#InitGrid, 10) = WAIT_TIMEOUT do
begin
FrmProgress.refresh;
Application.ProcessMessages;
end;
but obviously not compile because the type returned by LocalAsyncVclCall is IAsyncCall and not a Cardinal.
also i tried this, but not works because the initgrid procedure is executed but not asynchronously.
while not LocalAsyncVclCall(#InitGrid, 10).Finished do
begin
FrmProgress.refresh;
//Application.ProcessMessages;
end;
How i can use LocalAsyncVclCall or another function to execute an VCL code asynchronously .
i want something like this.
while ExecuteMyVCLProcedure(#InitGrid) = WAIT_TIMEOUT do
begin
FrmProgress.refresh;
//Application.ProcessMessages;
end;
UPDATE
The InitGrid procedure goes here, the TcxGrid does not provide any event to show the progress of the data load. because that i want to execute this procedure asynchronously.
procedure InitGrid;
begin
cxGrid1DBTableView1.BeginUpdate;
try
cxGrid1DBTableView1.DataController.DataSource:=DataSource1;//in this point assign the query previously executed to the grid.
cxGrid1DBTableView1.ClearItems;
cxGrid1DBTableView1.DataController.CreateAllItems;
for ColIndex:=0 to cxGrid1DBTableView1.ColumnCount-1 do
cxGrid1DBTableView1.Columns[ColIndex].Options.Editing:=False;
finally
cxGrid1DBTableView1.EndUpdate;
end;
end;
Thanks in advance.
UPDATE The InitGrid procedure goes
here, the TcxGrid does not provide any
event to show the progress of the data
load. because that i want to execute
this procedure asynchronously
You can't populate the cxGrid in a different VCL thread because there is only one VCL Thread, the MainThread. Calling LocalAsyncVclCall from the MainThread does nothing else than executing the given function in the thread that calls LocalAsyncVclCall. So it wouldn't do anything different than calling the InitGrid() function in the same thread where your ProcessMessages() call is. Hence ProcessMessages() would be called after the data was loaded into the cxGird what is not what you want.
All the *VclCall() functions are intended to be executed from a different thread than the MainThread.
Without changing the cxGrid's code to support a "progress event" you are out of luck with your attempt.
Use PostMessage to send a custom windows message to FrmProgress.
e.g
WM_PopulateGrid = WM_USER + 1;
procedure WMPopulateGrid(var msg: TMessage); message WM_PopulateGrid;
begin
//load records into the grid.
//You maybe can use a global variable to pass the records from RunQuery
....
end;