Механизм исключений потока Delphi

У меня есть дилемма о том, как работают потоки в delphi, и почему в момент, когда поток должен вызвать исключение, исключение не отображается. Ниже приведен код с комментариями, возможно, кто-нибудь объяснит мне, как этот поток или delphi управляют нарушениями прав доступа.

//код потока

unit Unit2;

interface

uses
  Classes,
  Dialogs,
  SysUtils,
  StdCtrls;

type
  TTest = class(TThread)
  private
  protected
    j: Integer;
    procedure Execute; override;
    procedure setNr;
  public
    aBtn: tbutton;
  end;

implementation


{ TTest }

procedure TTest.Execute;
var
  i                 : Integer;
  a                 : TStringList;
begin
 // make severals operations only for having something to do
  j := 0;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;
  for i := 0 to 100000000 do
    j := j + 1;

  Synchronize(setnr);
  a[2] := 'dbwdbkbckbk'; //this should raise an AV!!!!!!

end;

procedure TTest.setNr;
begin
  aBtn.Caption := IntToStr(j)
end;

end.

код проекта

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,
  Unit2, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  public
    nrthd:Integer;
    acrit:TRTLCriticalSection;
    procedure bla();
    procedure bla1();
    function bla2():boolean;
    procedure onterm(Sender:TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.bla;
begin
 try
  bla1;
 except on e:Exception do
   ShowMessage('bla '+e.Message);
 end;
end;

procedure TForm1.bla1;
begin
 try
  bla2
 except on e:Exception do
   ShowMessage('bla1 '+e.Message);
 end;
end;

function TForm1.bla2: boolean;
var ath:TTest;
begin
 try
  ath:=TTest.Create(true);
   InterlockedIncrement(nrthd);
  ath.FreeOnTerminate:=True;
  ath.aBtn:=Button1;
  ath.OnTerminate:=onterm; 
   ath.Resume;
 except on e:Exception do
  ShowMessage('bla2 '+e.Message);
 end;
end;

procedure TForm1.Button1Click(Sender: TObject);

begin
//
 try
   bla;
   while nrthd>0 do
    Application.ProcessMessages;
 except on e:Exception do
  ShowMessage('Button1Click '+e.Message);
 end;
 ShowMessage('done with this');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 nrthd:=0;
end;

procedure TForm1.onterm(Sender: TObject);
begin
 InterlockedDecrement(nrthd)
end;

end.

цель этого приложения только знать, где ловится нарушение прав доступа, и как должен быть написан код.
Я не могу понять, почему в строке "a[2] := 'dbwdbkbckbk';" АВ не поднимается.


person RBA    schedule 02.09.2010    source источник
comment
Разве отладчик не сообщил вам об исключении?   -  person Rob Kennedy    schedule 02.09.2010
comment
Переменная a не инициализирована! Что, если он указывает на ДЕЙСТВИТЕЛЬНЫЙ адрес памяти? Я имею в виду адрес, которым владеет процесс. Тогда ваш код будет записываться в этом месте без нарушения прав доступа. Я прав? Я думаю, что вы должны по крайней мере поставить NIL.   -  person Z80    schedule 12.02.2019
comment
См. комментарий Реми Лебо здесь. Он объясняет, что такое AV и как он возникает: stackoverflow.com/a/16071764/46207   -  person Z80    schedule 12.02.2019


Ответы (4)


В Delphi 2005 — и, вероятно, в большинстве других версий — если исключение выходит из метода Execute без обработки, оно перехватывается функцией, вызвавшей Execute, и сохраняется в свойстве FatalException потока. (См. Classes.pas, ThreadProc.) С этим исключением больше ничего не делается, пока не будет освобожден поток, после чего освобождается и исключение.

Поэтому вы обязаны проверить это имущество и что-то с этим сделать. Вы можете проверить это в обработчике потока OnTerminate. Если он не нулевой, то поток завершается из-за неперехваченного исключения. Так, например:

procedure TForm1.onterm(Sender: TObject);
var
  ex: TObject;
begin
  Assert(Sender is TThread);
  ex := TThread(Sender).FatalException;
  if Assigned(ex) then begin
    // Thread terminated due to an exception
    if ex is Exception then
      Application.ShowException(Exception(ex))
    else
      ShowMessage(ex.ClassName);
  end else begin
    // Thread terminated cleanly
  end;
  Dec(nrthd);
end;

Нет необходимости во взаимосвязанных функциях для отслеживания количества потоков. И функция создания потока, и обработчик завершения всегда выполняются в контексте основного потока. Достаточно старых Inc и Dec.

person Rob Kennedy    schedule 02.09.2010
comment
+1. Я никогда не видел FatalException?... о, подождите, мы все еще на Delphi 5. - person Lieven Keersmaekers; 02.09.2010
comment
У меня больше нет исходного кода для этой версии, @Lieven. Если у него есть AcquireExceptionObject, то вы можете сами имитировать новое поведение FatalException. - person Rob Kennedy; 02.09.2010
comment
@Lieven: Я думаю, что в D5 и D6 метод выполнения потока еще не был защищен ... Вы должны были сделать это самостоятельно в переопределении Execute. - person Marjan Venema; 02.09.2010
comment
Свойство TThread.FatalException (и функция System.AcquireExceptionObject()) было введено в D6. В D5 Execute() обертывается только try..finally, чтобы гарантировать, что DoTerminate() всегда вызывается, но поток завершается (через EndThread()) до того, как возникшее исключение будет отправлено обработчикам за пределами finally. В D6 TThread.Execute() обернут try..except, который получает исключение и сохраняет его в FatalException перед вызовом DoTerminate(). - person Remy Lebeau; 16.08.2016
comment
Работает на Delphi 10.1 Берлин - person alitrun; 11.06.2017
comment
Спасибо @alitrun, со времен d5 есть небольшие отличия, которые складываются друг с другом через версии, каждая часть информации должна быть указана с указанием версии ... трудно понять, когда неверсионное решение должно работать. - person Darkendorf; 03.01.2018

Многопоточность — это место, где вы должны пропускать исключения.

Суть обработки исключений в потоках заключается в том, что если вы хотите, чтобы исключение было показано конечному пользователю, вы должны захватить его и передать в основной поток, где его можно безопасно показать.

Вы найдете несколько примеров в этой ветке EDN Как обрабатывать исключения в объектах TThread.

procedure TMyThread.DoHandleException;
begin
  // Cancel the mouse capture
  if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);
  // Now actually show the exception
  if FException is Exception then
    Application.ShowException(FException)
  else
    SysUtils.ShowException(FException, nil);
end;

procedure TMyThread.Execute;
begin
  FException := nil;
  try
    // raise an Exception
    raise Exception.Create('I raised an exception');
  except
    HandleException;
  end;
end;

procedure TMyThread.HandleException;
begin
  // This function is virtual so you can override it
  // and add your own functionality.
  FException := Exception(ExceptObject);
  try
    // Don't show EAbort messages
    if not (FException is EAbort) then
      Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;
person Lieven Keersmaekers    schedule 02.09.2010

Мы также можем повторно вызвать FatalException. Повторное повышение кажется нелогичным, но если у вас есть центральный обработчик исключений/ошибок в вашем коде, и если вы просто хотите включить исключения потоков в этот механизм, вы можете повторно поднять в некоторых редких случаях:

procedure TForm1.onterm(Sender: TObject);
var
  ex: Exception;
begin
  Assert(Sender is TThread);
  ex := Exception(TThread(Sender).FatalException);
  if Assigned(ex) then
    // Thread terminated due to an exception
    raise ex;
  Dec(nrthd);
end;
person Ömür Ölmez    schedule 30.03.2011
comment
Вы уверены, что это работает? Как насчет освобождения этого объекта исключения? - person dummzeuch; 18.08.2011
comment
Это не работает (по крайней мере, в XE2). Я попробовал это и получил немодальное диалоговое окно сообщения, за которым следует EOSError. - person Jens Mühlenhoff; 19.08.2013
comment
Обработчик OnTerminate вызывается Synchronize() внутри контекста рабочего потока после выхода Execute(). Если внутри Synchronize() возникает исключение, оно получает исключение и повторно вызывает его в контексте рабочего потока. Поэтому создание исключения внутри OnTerminate — это плохо. Чтобы сделать это правильно, вам нужно было бы вручную взять на себя ответственность за FatalException, каким-то образом сбросить TThread.FFatalException в nil (чтобы TThread не уничтожал объект) и самостоятельно повторно поднять его в основном потоке после выхода Synchronize(). Хаки необходимы, чтобы стать владельцем - person Remy Lebeau; 16.08.2016

Переменная "a" не инициализирована! Он может указывать на ЛЮБУЮ возможную ячейку памяти вашего компьютера. Он может даже указывать на местоположения, которые физически не существуют (хотя это спорно из-за системы виртуальной памяти).

Итак, в вашей программе, если «a» случайно указывает на ДЕЙСТВИТЕЛЬНЫЙ адрес памяти (я имею в виду адрес, которым владеет процесс), тогда ваш код будет записываться в это место без нарушения прав доступа. Я думаю, что вы должны по крайней мере поставить «а» на NIL.

См. комментарий Реми Лебо здесь: http://www.stackoverflow.com/a/16071764/46207
И это также: Почему неинициализированные указатели вызывают нарушения доступа к памяти close на 0?

person Z80    schedule 12.02.2019