1

I have a number crunching application with a TExecution class that is included in a separate unit Execution.pas and carries out all the calculations. The class instances are created from the main form of the program. Very often the code in Execution.pas needs to run 10-15 times in a row and I want to create several TExecution instances in different threads and run them in parallel. A simplified version of the code is as follows:

Main Form with one Button1 in it:

unit MainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Threading, Execution;

type
  TMainForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  MainForm1: TMainForm1;

implementation

{$R *.dfm}

procedure TMainForm1.Button1Click(Sender: TObject);
var
  ExecutionThread: array of TThread;
  NoThreads: integer;
  Execution: array of TExecution;
  thread_ID: integer;
begin
    NoThreads := 5;
    SetLength(Execution,NoThreads);
    SetLength(ExecutionThread,NoThreads);
    //----------------------------------------------------------------------------------
   for thread_ID := 0 to Pred(NoThreads) do
    begin
        ExecutionThread[thread_ID] := TThread.CreateAnonymousThread(
        procedure
        begin
            try
                Execution[thread_ID] := TExecution.Create;
                Execution[thread_ID].CalculateSum;
            finally
                if Assigned(Execution[thread_ID]) then
                begin
                    Execution[thread_ID] := nil;
                    Execution[thread_ID].Free;
                end;
            end;
        end);
        ExecutionThread[thread_ID].FreeOnTerminate := true;
        ExecutionThread[thread_ID].Start;
    end;

end;

end.

Execution.pas unit:

unit Execution;

interface
uses
System.SysUtils, Vcl.Dialogs, System.Classes, WinApi.Windows;

 type
   TExecution = Class
      const
        NoOfTimes = 1000000;
      var
        Sum: integer;
      private
        procedure IncrementSum(var Sum: integer);
      published
        procedure CalculateSum;
   End;

implementation

procedure TExecution.CalculateSum;
var
  i: integer;
begin
    Sum := 0;
    for i := 0 to Pred(NoofTimes) do
    begin
        IncrementSum(Sum);
    end;
end;

procedure TExecution.IncrementSum(var Sum: integer);
begin
    Inc(Sum);
end;

end.

Whenever I run the code above by clicking Button1 the TExecution instances run, but when I close the program, I get an Access Violation in GetMem.inc in function SysFreeMem. Obviously, the code messes up the memory, I guess it is because of the parallel memory allocation, but I was unable to find the cause and fix a solution to it. I note that with one thread (NoThreads := 1), or with a serial execution of the code (either with a single new thread and 5 TExecution instances, or when the instances of TExecution are created directly from MainForm), I do not get similar memory problems. What is the problem with my code? Many thanks in advance!

  • 1
    Why not `TExecution = Class( TThread )` right away? – AmigoJack Jul 22 '21 at 14:35
  • Simply because I have not thought about it (and I never thought that this was possible). How can one create the thread, call the calculation procedure and then destroy the class? The sytnax is the same, but it creates the new class in a new thread? – Stelios Antoniou Jul 22 '21 at 14:44
  • 2
    That's [class inheritance](http://docwiki.embarcadero.com/RADStudio/Sydney/en/Classes_and_Objects_(Delphi)#Inheritance_and_Scope): you'll instanciate objects from `TExecute` which is an extension of a `TThread`, just with your additional variables/procedures. Welcome to OOP. – AmigoJack Jul 22 '21 at 15:22

1 Answers1

1

The problem comes from ExecutionThread and Execution which are local variables. When all threads are started, the procedure Button1Click exits, the two variables are freed, long before threads are terminated.

Move the two variables ExecutionThread and Execution to the TMainForm1 field and your problem will be gone. Of course: if you close the program before the threads are terminated, you'll be again in trouble.

Also, invert the two lines:

Execution[thread_ID] := nil;
Execution[thread_ID].Free;

You must free before niling.

BTW: You should get a compiler warning about published in TExecution.

EDIT: Following the comment on this answer, here is the code for the same process but using an explicit worker thread and a generic TList to maintain the list of running thread.

Source for the main form:

unit ThreadExecutionDemoMain;

interface

uses
    Winapi.Windows, Winapi.Messages,
    System.SysUtils, System.Variants, System.Classes,
    System.Generics.Collections,
    Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
    ThreadExecutionDemoExecution,
    ThreadExecutionDemoWorkerThread;

type
    TMainForm = class(TForm)
        StartButton: TButton;
        DisplayMemo: TMemo;
        procedure StartButtonClick(Sender: TObject);
    private
        ThreadList : TList<TWorkerThread>;
        procedure WrokerThreadTerminate(Sender : TObject);
    public
        constructor Create(AOwner : TComponent); override;
        destructor  Destroy; override;
    end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

constructor TMainForm.Create(AOwner: TComponent);
begin
    ThreadList := TList<TWorkerThread>.Create;
    inherited Create(AOwner);
end;

destructor TMainForm.Destroy;
begin
    FreeAndNil(ThreadList);
    inherited Destroy;;
end;

procedure TMainForm.StartButtonClick(Sender: TObject);
var
    NoThreads    : Integer;
    ID           : Integer;
    WorkerThread : TWorkerThread;
begin
    NoThreads := 5;
    for ID := 0 to Pred(NoThreads) do begin
        WorkerThread := TWorkerThread.Create(TRUE);
        WorkerThread.ID          := ID;
        WorkerThread.OnTerminate := WrokerThreadTerminate;
        WorkerThread.FreeOnTerminate := TRUE;
        ThreadList.Add(WorkerThread);
        DisplayMemo.Lines.Add(Format('Starting thread %d', [WorkerThread.ID]));
        WorkerThread.Start;
    end;
    DisplayMemo.Lines.Add(Format('There are %d running threads', [ThreadList.Count]));
end;

procedure TMainForm.WrokerThreadTerminate(Sender: TObject);
var
    WorkerThread : TWorkerThread;
begin
    WorkerThread := TWorkerThread(Sender);
    ThreadList.Remove(WorkerThread);
    // This event handler is executed in the context of the main thread
    // we can access the user interface directly
    DisplayMemo.Lines.Add(Format('Thread %d done. Sum=%d',
                                 [WorkerThread.ID, WorkerThread.Sum]));
    if ThreadList.Count = 0 then
        DisplayMemo.Lines.Add('No more running threads');
end;

end.

Source for the execution unit:

unit ThreadExecutionDemoExecution;

interface

type
    TExecution = class
    const
        NoOfTimes = 1000000;
    private
        FSum: Integer;
        procedure IncrementSum(var ASum: Integer);
    public
        procedure CalculateSum;
        property Sum: Integer    read  FSum
                                 write FSum;
    end;


implementation

{ TExecution }

procedure TExecution.CalculateSum;
var
    I: Integer;
begin
    FSum := 0;
    for I := 0 to Pred(NoOfTimes) do
        IncrementSum(FSum);
end;

procedure TExecution.IncrementSum(var ASum: Integer);
begin
    Inc(ASum);
end;

end.

Source for the worker thread:

unit ThreadExecutionDemoWorkerThread;

interface

uses
    System.SysUtils, System.Classes,
    ThreadExecutionDemoExecution;

type
    TWorkerThread = class(TThread)
    private
        FExecution : TExecution;
        FID        : Integer;
        FSum       : Integer;
    protected
        procedure Execute; override;
    public
        property ID        : Integer    read  FID
                                        write FID;
        property Sum       : Integer    read  FSum
                                        write FSum;
    end;


implementation

{ TWorkerThread }

procedure TWorkerThread.Execute;
begin
    FExecution := TExecution.Create;
    try
        FExecution.CalculateSum;
        FSum := FExecution.Sum;
    finally
        FreeAndNil(FExecution);
    end;
end;

end.
fpiette
  • 11,983
  • 1
  • 24
  • 46
  • Thanks @fpiette. I tried it, but I get exactly the same behaviour. I tried adding the the variables to the TMainForm1 class (as either private or public variables) or as variables in the MainForm unit. In all cases, the behaviour does not change. – Stelios Antoniou Jul 22 '21 at 14:35
  • Also the sequence of nil and free lines does not play a role in that particular problem. Btw, I have read this link: https://stackoverflow.com/questions/26623852/why-freeandnil-implementation-doing-nil-before-free where it mentons that in FreeAndNil the nil comes first before freeing the memory. – Stelios Antoniou Jul 22 '21 at 14:36
  • 1
    Be sure to have all thread terminated before leaving the program. Change the number of iterations to a smaller value if you don't want to wait for the threads and still being sure they are done. – fpiette Jul 22 '21 at 15:01
  • 1
    FreeAndNil first save the address, then set to nil, then free the saved address. That's not what you do: you first nil and loose the address then call Free using the nil address and this won't free the memory. – fpiette Jul 22 '21 at 15:02
  • Thanks @fpiette. The execution of each thread lasts for less than 1 sec. The threads should have been terminated with FreeOnTerminate long before I close the program (at least in theory) , am I right? – Stelios Antoniou Jul 22 '21 at 15:08
  • How long it takes depends on your computer. FreeOnTerminate DO NO TERMINATE the thread. It will free the thread once it has terminated. If you want to debug what happens, use OutputDebugString that you can call even from a thread. Messages are shown in the IDE event window (Ctrl+Alt+V). – fpiette Jul 22 '21 at 15:11
  • Sure, but the threads should have been terminated and freed before I close the program. The same behavour happens if the loop runs only 10 or 20 times (or just once). There should be something related to the initialization or the freeing of the instances of the threads or the instances of TExecution (possibly the combination of the two), which I cannot understand. – Stelios Antoniou Jul 22 '21 at 15:22
  • 1
    I used your code with the change I described and it works here using Delphi 10.4.2. Maybe the code you showed is not the real code you use or you use an older Delphi which has a different behavior. Maybe try with an explicit thread (A class inheriting from TThread) instead of an anonymous thread. – fpiette Jul 22 '21 at 16:16
  • Many thanks. I am using Delphi 10.4.1. It is recent, I do not think my problems are related to the Delphi version. The code is the real code (I copied it and pasted it directly to the post), I had no reason to change it. I will keep on searching, I cannot understand why in my case the code gives the Access Violation. – Stelios Antoniou Jul 22 '21 at 16:36
  • I will also try to create a class inheriting from TThread. I tried it quickly, I got a TAbstractError, but honestly I need some background reading before I get confident with what I am doing and ask any solid questions. Many thanks again @fpiette, the fact that the code works for you is a great hint! I will keep on trying... – Stelios Antoniou Jul 22 '21 at 16:37
  • 1
    You'd better restart with the code I wrote. – fpiette Jul 22 '21 at 19:51
  • I confirm that your code works flawlessly! Of course, I now have to understand why the initial code was not running, but thanks to you I have some solid code to start working with! Cheers! – Stelios Antoniou Jul 22 '21 at 21:21