Повышение исключения в TThread Execute?

Я только что понял, что мои исключения не показываются пользователю в моих темах!

Сначала я использовал это в своей теме для создания исключения, которое не работает:

except on E:Exception do
begin
  raise Exception.Create('Error: ' + E.Message);
end;

IDE показывает мне исключения, а мое приложение - нет!

Я искал решение, вот что я нашел:

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

http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_22039681.html

И ни один из них не работал для меня.

Вот мой блок темы:

unit uCheckForUpdateThread;

interface

uses
  Windows, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
  IdHTTP, GlobalFuncs, Classes, HtmlExtractor, SysUtils, Forms;

type
  TUpdaterThread = class(TThread)
  private
    FileGrabber : THtmlExtractor;
    HTTP : TIdHttp;
    AppMajor,
    AppMinor,
    AppRelease : Integer;
    UpdateText : string;
    VersionStr : string;
    ExceptionText : string;
    FException: Exception;
    procedure DoHandleException;
    procedure SyncUpdateLbl;
    procedure SyncFinalize;
  public
    constructor Create;

  protected
    procedure HandleException; virtual;

    procedure Execute; override;
  end;

implementation

uses
  uMain;

{ TUpdaterThread }

constructor TUpdaterThread.Create;
begin
  inherited Create(False);
end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;

  if Terminated then
    Exit;

  FileGrabber           := THtmlExtractor.Create;
  HTTP                  := TIdHTTP.Create(nil);
  try
    try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    except on E: Exception do
    begin
      UpdateText := 'Error while updating xSky!';
      ExceptionText := 'Error: Cannot find remote file! Please restart xSky and try again! Also, make sure you are connected to the Internet, and that your Firewall is not blocking xSky!';
      HandleException;
    end;
    end;

    try
      AppMajor      := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor      := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease    := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    except on E:Exception do
    begin
      HandleException;
    end;
    end;

    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then
    begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;

  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);
end;

procedure TUpdaterThread.SyncFinalize;
begin
  DoTransition(frmMain.TransSearcher3, frmMain.gbLogin, True, 500);
end;

procedure TUpdaterThread.SyncUpdateLbl;
begin
  frmMain.lblCheckingForUpdates.Caption := UpdateText;
end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TUpdaterThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

end.

Если вам нужно больше информации, просто дайте мне знать.

Опять же: IDE перехватывает все исключения, но моя программа их не показывает.

РЕДАКТИРОВАТЬ: Это было решение Cosmin, которое работало в конце - и причина, по которой это не поначалу, было потому, что я не добавил переменную ErrMsg, вместо этого я просто поместил все, что переменная будет содержать в Synchronize, что не будет работать Однако я понятия не имею, почему. Я понял это, когда у меня не было других идей, и я просто возился с решениями.

Как всегда, шутка на меня. = Р

6 ответов

Решение

Вот мое очень, очень короткое "мнение" по этому вопросу. Он работает только на Delphi 2010+ (потому что эта версия представила анонимные методы). В отличие от более сложных методов, уже опубликованных, мой показывает только сообщение об ошибке, ни больше, ни меньше.

procedure TErrThread.Execute;
var ErrMsg: string;
begin
  try
    raise Exception.Create('Demonstration purposes exception');
  except on E:Exception do
    begin
      ErrMsg := E.ClassName + ' with message ' + E.Message;
      // The following could be all written on a single line to be more copy-paste friendly  
      Synchronize(
        procedure
        begin
          ShowMessage(ErrMsg);
        end
      );
    end;
  end;
end;

Что-то очень важное, что вам нужно понять о многопоточной разработке:

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

Потоки могут взаимодействовать друг с другом только определенным образом:

  • Они могут работать с общими данными или объектами. Это может привести к проблемам параллелизма "гоночные условия", и поэтому вы должны быть в состоянии помочь им "красиво делиться данными". Что подводит нас к следующему пункту.
  • Они могут "сигнализировать друг другу", используя различные подпрограммы поддержки ОС. К ним относятся такие вещи, как:
    • Мьютексы
    • Критические разделы
    • События
  • И, наконец, вы можете отправлять сообщения в другие темы. При условии, что поток каким-то образом написан как получатель сообщения.

Примечание: обратите внимание, что потоки не могут строго говоря вызывать другие потоки напрямую. Если, например, поток A попытается вызвать поток B напрямую, это будет шагом в стеке вызовов потока A!

Это подводит нас к теме вопроса: "в моих темах не возникают исключения"

Причина этого заключается в том, что все исключение:

  • Запишите ошибку
  • И раскрутите стек вызовов. <- NB: Ваш экземпляр TThread не может разматывать стек вызовов основного потока и не может произвольно прерывать выполнение основных потоков.

Таким образом, TThread не будет автоматически сообщать об исключениях в ваше основное приложение.

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

Решение

  • Первый шаг такой же, как в однопоточном приложении. Вам нужно решить, что означает ошибка и как должен реагировать поток.
    • Должен ли поток продолжить обработку?
    • Должна ли нить прерваться?
    • Должна ли ошибка регистрироваться / сообщаться?
    • Ошибка требует решения пользователя? <- Это наиболее сложная реализация, поэтому мы пока пропустим ее.
  • Как только это будет решено, реализуйте соответствующий обработчик исключений.
  • TIP: Make sure the exception doesn't escape the thread. The OS won't like you if it does.
  • Если вам нужна основная программа (поток), чтобы сообщить об ошибке пользователю, у вас есть несколько вариантов.
    • Если поток был написан так, чтобы он возвращал объект результата, то это легко: внести изменения, чтобы он мог вернуть ошибку в этом объекте, если что-то пошло не так.
    • Отправьте сообщение в основную ветку, чтобы сообщить об ошибке. Обратите внимание, что основной поток уже реализует цикл сообщений, поэтому ваше приложение сообщит об ошибке, как только обработает это сообщение.

РЕДАКТИРОВАТЬ: Пример кода для указанного требования.

Если все, что вы хотите сделать, это уведомить пользователя, то ответ Cosmind Prund должен отлично работать для Delphi 2010. Старые версии Delphi требуют немного больше работы. Следующее концептуально похоже на собственный ответ Джеффа, но без ошибок:

procedure TUpdaterThread.ShowException;
begin
  MessageDlg(FExceptionMessage, mtError, [mbOk], 0);
end;

procedure TUpdaterThread.Execute;
begin
  try

    raise Exception.Create('Test Exception');
    //The code for your thread goes here
    //
    //

  except
    //Based on your requirement, the except block should be the outer-most block of your code
    on E: Exception do
    begin
      FExceptionMessage := 'Exception: '+E.ClassName+'. '+E.Message;
      Synchronize(ShowException);
    end;
  end;
end;

Некоторые важные исправления в ответе Джеффа, включая реализацию, показанную в его вопросе:

Призыв к Terminate уместно, только если ваш поток реализован в while not Terminated do... петля Посмотрите на то, что Terminate метод на самом деле делает.

Призыв к Exit это ненужная трата, но вы, вероятно, сделали это из-за вашей следующей ошибки.

В своем вопросе вы оборачиваете каждый шаг по-своему try...except обработать исключение. Это абсолютно нет-нет! Делая это, вы притворяетесь, что, несмотря на исключение, все в порядке. Ваш поток пробует следующий шаг, но на самом деле гарантированно потерпит неудачу! Это не способ обработки исключений!

Потоки не распространяют исключения автоматически в другие потоки. Так что вы должны разобраться с этим сами.

Рафаэль изложил один подход, но есть альтернативы. Решение Рафаэль указывает синхронно на исключительную ситуацию, направляя ее в основной поток.

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

Код выглядит так.

procedure TMyThread.Execute;
begin
  Try
    DoStuff;
  Except
    on Exception do begin
      FExceptAddr := ExceptAddr;
      FException := AcquireExceptionObject;
      //FBugReport := GetBugReportCallStackEtcFromMadExceptOrSimilar.
    end;
  End;
end;

Если управляющий поток решает вызвать исключение, он может сделать это следующим образом:

raise Thread.FException at Thread.FExceptAddr;

Иногда у вас может быть код, который не может вызвать Synchronize, например, некоторые библиотеки DLL, и этот подход полезен.

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

Что ж,

Без вашего исходного кода будет тяжело, но я проверил это:

Как обрабатывать исключения в объектах TThread

И это прекрасно работает. Возможно, вам стоит взглянуть на это.

РЕДАКТИРОВАТЬ:

Вы не следите за тем, на что указывают ссылки, на которые вы указываете. Проверьте мою ссылку, и вы увидите, как это сделать.

РЕДАКТИРОВАТЬ 2:

Попробуйте и скажите, сработало ли это:

 TUpdaterThread= class(TThread)
 private
   FException: Exception;
   procedure DoHandleException;
 protected
   procedure Execute; override;
   procedure HandleException; virtual;
 end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;
  if Terminated then
    Exit;
  FileGrabber := THtmlExtractor.Create;
  HTTP := TIdHTTP.Create(Nil);
  try
    Try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    Except
      HandleException;
    End;
    Try
      AppMajor := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    Except
      HandleException;
    End;
    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;
  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);

end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TMyThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

РЕДАКТИРОВАТЬ 3:

Вы сказали, что не можете перехватить EIdHTTPProtocolException. Но это работает для меня. Попробуйте этот пример и убедитесь сами:

procedure TUpdaterThread.Execute;
begin
  Try
    raise EIdHTTPProtocolException.Create('test');
  Except
    HandleException;
  End;
end;

Ранее я использовал SendMessge для связи между потоками с использованием TWMCopyData, поэтому я думаю, что должно работать следующее:

Const MyAppThreadError = WM_APP + 1;

constructor TUpdaterThread.Create(ErrorRecieverHandle: THandle);
begin
    Inherited Create(False);
    FErrorRecieverHandle := Application.Handle;
end;

procedure TUpdaterThread.Execute;
var
    cds: TWMCopyData;
begin
  try
     DoStuff;
  except on E:Exception do
    begin
        cds.dwData := 0;
        cds.cbData := Length(E.message) * SizeOf(Char);
        cds.lpData := Pointer(@E.message[1]);         
        SendMessage(FErrorRecieverHandle, MyAppThreadError, LPARAM(@cds), 0);
    end;
  end;
end;

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

Вам нужно будет добавить Self.Handle чтобы конструктор в форме создал поток и обработал сообщение в форме, которая его создала

procedure HandleUpdateError(var Message:TMessage); message MyAppThreadError;
var
    StringValue: string;
    CopyData : TWMCopyData; 
begin
    CopyData := TWMCopyData(Msg);
    SetLength(StringValue, CopyData.CopyDataStruct.cbData div SizeOf(Char));
    Move(CopyData.CopyDataStruct.lpData^, StringValue[1], CopyData.CopyDataStruct.cbData);
    Message.Result := 0;
    ShowMessage(StringValue);
end;

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

Другие вопросы по тегам