multithreadingdelphiaccess-violation

c00000005 ACCESS_VIOLATION when exiting application while thread is running


c00000005 ACCESS_VIOLATION caused when closing the Form (exiting application) while the following thread is running (this is just an EXAMPLE, to illustrate the problem):

type
  Twtf = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure Twtf.Execute;
const
  hopefully_big_enough_to_trigger_problem = 100000000;
var
  A: array of Integer;
  I: Integer;
begin
  SetLength(A, hopefully_big_enough_to_trigger_problem);
  while not Terminated do begin
    for I := Low(A) to High(A) do
      A[I] := 0;
  end;
end;

var
  wtf: Twtf;

procedure TForm1.Button4Click(Sender: TObject);
begin
  wtf := Twtf.Create;
  wtf.FreeOnTerminate := True;
end;

What is happening according to the Local Variables debug window is that A is now (), namely Length(A) is 0. Obviously this is why accessing A[I] causes problems, but note that I did not manually do anything to A.

How do I stop this error from showing (does not necessarily have to be prevented from happening)? The application is closing, the thread should just die... QUIETLY. Which actually happens for smaller arrays, but with a sufficiently large one 1 or 2 out of every 3 attempts results in the problem (if run with debugging it is shown always; i.e., this always happens, but is sometimes simply hidden).

The following does not help:

destructor TForm1.Destroy;
begin
  wtf.terminate;
  inherited;
end;

Only doing TerminateThread(wtf.Handle, 0); in the destructor instead solves (hides) the problem. But surely there is a more graceful way?


Solution

  • To reiterate:

    This was an example crafted to illustrate what happens when a thread loop is accessing a dynamic array when the application is terminated (regardless of whatever graceful shutdown mechanisms, the thread may be within the loop at that point in time, and blocking the application while waiting a possibly long while for it to finish or get to a if Terminated check is not optimal for obvious reasons).

    The problem is caused by the application freeing the array's memory before it frees the thread, WHILE the thread is working on the array (the array is a variable belonging to the thread); e.g., with a sufficiently large multidimensional A: array of array of Integer, it is actually possible to see A[500000] freed to () while A[400000] is still fine.

    The solution is so simple as to be trivial:

    Twtf = class(TThread)
    
    ...
    
    var
      wtf: Twtf;
    
    ...
    
    destructor TForm1.Destroy;
    begin
      // Only Form in the Application, otherwise add Application.Terminated check?
      if wtf <> nil then
        with wtf do
          if not Finished then
            Suspend;
      inherited;
    end;
    

    This eliminates the race condition between the thread working on its array and the application freeing that array.

    Note that the thread will NOT call its destructor, so resources (if necessary) should be freed manually.

    All of the above is irrespective of FreeOnTerminate.

    EDIT: Added Finished check

    EDIT: Alternative to deprecated Suspend: SuspendThread(Handle);