Threading inconsistency Delphi xe6
Asked Answered
P

0

6

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;
Pace answered 26/8, 2014 at 16:58 Comment(20)
This question is nearly there but we probably need to see an mcve for the failing code.Aiden
So, half the code in this question is largely irrelevant. Code that works works because it has no errors. Your code does not work because it has errors. A huge paste of irrelevant code with no errors is not helpful. As for your code, I don't see any exception handling in your Execute method (big red flag!), we don't know what the login() method is doing, and it seems you've commented out the rest of your actual code... Suggest you delete the snip from the Emba samples, show the rest of your actual code, and then maybe this question will start to become answerable...Mandatory
Why are you calling "inherited" in your thread's Execute Method?Radish
The difference is that your code shows a form from within Execute (in the call to Synchronize) whereas the other code does not. If you're attempting to execute a login from the thread before your app starts, that's totally unnecessary. There are much better ways to accomplish that which don't require a separate thread at all. Threads should never need to directly interact with the UI, and should never need to totally interrupt it by displaying a form requiring input. If your thread needs input from the user, get it first and then provide it to the thread when it starts.Fulk
@Ken I'm not sure how you concluded most of what you wrote? You can see the click event where I initialize the thread, pulling the input data from form data.. I think that should imply the app is running the 'login' form is already showing... The Login function is just a procedure to make sure the username/pass is okay in the database. I use a simple post call with the indy components. @J I included a lot of code from the 'irrelevant' anon unit because quite frankly, there are two types of visitors: lazy people, and people who want to see clean questions (like you). I figured it best to keepPace
the 'lazy' people from skimming over the question because they don't feel like finding the anon unit or sample. I also make multiple references to exception handling, so i apologize if my assumption in your assumption that I am already taking precautionary measures is wrong... (if that makes sense?). I will work on a way to make my question more clear, but it still stands as an obvious question for... WHY: do I have to use a sync timer that calls 'checksynchronize' with my code and not the anon unit?Pace
Okay ^ I get it now. Hmm... So bare with me. So with the sample, I'm more so passing a pointer to a function that handles the GUI instead of synchronizing to handle gui ( like I did/do in the code I provided ). But I thought the whole point of the synchronize method is to enable handling FMX/VCL controls?Pace
Sorry, but the difference is still clear, exactly as I stated before. The sample code is not directly accessing visual controls, and yours is doing so. I didn't "conclude" anything; I'm just saying that the code you've written is logically flawed IMO. You should never have the need to display a form from a thread, whether it's via Synchronize or not. Your login function should launch the thread, and let it execute to completion, retrieve the success/failure of the login, and pass that back, and let the caller handle the result.Fulk
This all seems to be getting beside the point - there now seem to be two problems and two questions. First, this application seems to be crashing without explanation - this is problem #1. Without seeing the rest of the code in the Execute method of the thread, it is not possible to offer a diagnosis as to why this is happening. Calling Form.Show in a Synchronize method is ugly, but it should not break the application. Problem #2 now seems to be that you think you need to be calling CheckSynchronize in the UI thread (again, in code we can't see) for reasons we don't know.Mandatory
In addition to needing to see additional code, my remaining question is why do you think you need to call CheckSynchronize (you don't) - what happens if you don't call this method, how is it different to what you expect to happen, and how does calling it change anything? Again, we need to see the offending code - what are you doing in the Login method? What is happening where you have commented out code? Where are you calling CheckSynchronize?Mandatory
@j and @ken, I updated OPPace
When I dont have checkSynchronize within a timer, then the Thread starts as should, but what's in the Synchronize(procedure begin...) never does anythingPace
This comment makes me wonder.... // ...because if the searchbox has text, when attempting to add items then app crashes. Don't you find that curious? That's a bug right there. Why not fix it rather than hack around it? What handlers are attached to that search box?Mandatory
Also suggest you read and pay attention to the OSX section of : docwiki.embarcadero.com/Libraries/XE6/en/… - you haven't told us what platform, but if cross-platform this may be relevant.Mandatory
No handlers are attached to the search box and here is a reference to someone else having same problem #25248761Pace
possible duplicate of Application taking a really long time to synch threads ( or not at all )Mandatory
answer may be the same in that Thread's are still broken in Delphi but the question is not the same..Pace
It answers at least one of the questions in your question.Mandatory
Synchronize uses a sync event to synchronize control with the main thread. In message processing when a null message is processed or during idle processing the application will call CheckSynchronized. If you are required to call CheckSynchronized yourself in your main code (possibly in a loop or timer) then you may have something keeping your application from receiving the WM_NULL (perhaps code trying to handle it?)Exclaim
Use IXMLDocument and functions LoadXMLData, LoadXMLDocument and NewXMLDocument instead of TXMLDocument and constructor with without Owner.Seismography

© 2022 - 2024 — McMap. All rights reserved.