Search code examples
delphidelphi-10.2-tokyo

Delphi cannot execute procedure in Task with object references


I have a simple class that has the following interface implementation.

Note: TPolyBase is an abstract class and TPolyResult is an array of double; it's not important to see their code, it's not relevant here.

//INTERFACE

type
  TPolynomialList = class
    strict private
      FPolynomialList: TObjectList<TPolyBase>;
      FResult: TList<TPolyResult>;
      FCanGet: boolean;
      function GetResult: TList<TPolyResult>;
      procedure DoSolve;
    public
      constructor Create(PolynomialList: TObjectList<TPolyBase>);
      destructor Destroy; override;

      procedure SolvePolynomials(CompletionHandler: TProc);
      property Solutions: TList<TPolyResult> read GetResult;
  end;

//IMPLEMENTATION

constructor TPolynomialList.Create(PolynomialList: TObjectList<TPolyBase>);
begin
  FPolynomialList := PolynomialList;
  FResult := TList<TPolyResult>.Create;
  FCanGet := false;
end;

destructor TPolynomialList.Destroy;
begin
  FResult.Free;
  inherited;
end;

procedure TPolynomialList.DoSolve;
var
  i: integer;
begin
  for i := 0 to FPolynomialList.Count - 1 do
    FResult.Add(FPolynomialList[i].GetSolutions);

  FCanGet := true;
end;

function TPolynomialList.GetResult: TList<TPolyResult>;
begin
  if FCanGet = false then
    raise TEquationError.Create('You must solve the equation first!');

  Result := FResult;
end;

procedure TPolynomialList.SolvePolynomials(CompletionHandler: TProc);
begin
    TTask.Run(procedure
              var
                ex: TObject;
              begin
                try
                  DoSolve;
                  TThread.Synchronize(nil, procedure
                                           begin
                                             CompletionHandler;
                                           end);
                except
                  on E: Exception do
                    begin
                      ex := AcquireExceptionObject;
                      TThread.Synchronize(nil, procedure
                                           begin
                                             Writeln( (ex as Exception).Message );  
                                           end);
                    end;
                end;
              end);
end;

This class takes a list of objects as input and it has an internal important field called FResult that gives the results to the user. It can be accessed from the getter only if the method SolvePolynomials has finished his work.


The problem is in the SolvePolynomials. The code I have shown uses a task because the size of the object list may be very big and I don't want to freeze the UI. Why do I always get an access violation in the task code?

Note that the following code works fine but this is not what I want because if I input 15000 the program freezes for a few seconds.

procedure TPolynomialList.SolvePolynomials(CompletionHandler: TProc);
begin
  DoSolve;
  CompletionHandler;
end;

Could the FPolynomialList variable be a the problem? If you look at my class the only thing "taken from outside" is the TObjectList<TPolyBase> because in the constructor I simply assing the reference (I'd like to avoid the copy ok 15k items). All the other variables are not shared with anything.

I have seen in many books I have read like "Delphi High Performance" that is good to have a task that calls an inner "slow" method but in this case there could be those reference that are messing up something. Any idea?


This is the code that I am using as test:

var
 a: TObjectList<TPolyBase>;
 i, j: integer;
 f: TPolynomialList;
 s: string;

 function GetRandom: integer;
 begin
   Result := (Random(10) + 1);
 end;

begin
a := TObjectList<TPolyBase>.Create(true);
 try

   for i := 0 to 15000 do
     begin
       a.Add({*Descendant of TPolyBase*})
     end;


   f := TPolynomialList.Create(a);
   try
     f.SolvePolynomials(procedure
                        var
                          i, j: integer;
                        begin    
                          for i := 0 to f.Solutions.Count - 1 do
                            begin
                              for j := Low(f.Solutions[i]) to High(f.Solutions[i]) do
                                Writeln({output the results...})
                            end;  
                        end);
   finally
     f.Free;
   end;

 finally
   a.Free;
 end;

end.

Solution

  • Your SolvePolynomials method delegates solving to another thread and returns before that thread is finished with its task. While that task thread is running it is necessary that all data it operates on is still alive. But, in your code you are releasing necessary object instances right after SolvePolynomials exits - while your task is still running, hence the error.

    You have to move releasing of those objects into completion handler.

    Basically, your code simplified looks like:

    type
      TPolynomialList = class
      public
        destructor Destroy; override;
        procedure DoSolve;
        procedure SolvePolynomials(CompletionHandler: TProc);
      end;
    
    destructor TPolynomialList.Destroy;
    begin
      Writeln('Destroyed');
      inherited;
    end;
    
    procedure TPolynomialList.DoSolve;
    begin
      Writeln('Solving');
    end;
    
    procedure TPolynomialList.SolvePolynomials(CompletionHandler: TProc);
    begin
      TTask.Run(
      procedure
      begin
        try
          DoSolve;
          TThread.Synchronize(nil,
            procedure
            begin
              CompletionHandler;
            end);
        except
          on E: Exception do
            Writeln(E.ClassName, ': ', E.Message);
        end;
      end);
    end;
    
    procedure Test;
    var
      f: TPolynomialList;
    begin
      f := TPolynomialList.Create;
      try
        f.SolvePolynomials(
          procedure
          begin
            Writeln('Solved');
          end);
      finally
        f.Free;
      end;
    end;
    

    If you run it output will be:

    Destroyed
    Solving
    Solved
    

    However, if you move releasing of your variables into completion handler order of execution will be correct.

    procedure Test;
    var
      f: TPolynomialList;
    begin
      f := TPolynomialList.Create;
      f.SolvePolynomials(
        procedure
        begin
          Writeln('Solved');
          f.Free;
        end);
    end;
    
    Solving
    Solved
    Destroyed
    

    For your code, that means moving both a.Free and f.Free into completion handler.