Sollevare un'eccezione nell'esecuzione di TThread?
-
12-11-2019 - |
Domanda
Mi sono appena reso conto che le mie eccezioni non vengono mostrate all'utente nei miei thread!
All'inizio l'ho usato nel mio thread per sollevare l'eccezione, che non funziona:
except on E:Exception do
begin
raise Exception.Create('Error: ' + E.Message);
end;
L'IDE mi mostra le eccezioni, ma la mia app no!
Ho cercato una soluzione, questa è quella che ho trovato:
Meccanismo di eccezione del thread Delphi
http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_22039681.html
E nessuno di questi ha funzionato per me.
Ecco la mia unità di discussione:
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.
Se hai bisogno di maggiori informazioni fammi sapere.
Ancora:L'IDE rileva tutte le eccezioni, ma il mio programma non le mostra.
MODIFICARE:È stata la soluzione di Cosmin che ha funzionato alla fine - e il motivo per cui all'inizio non ha funzionato è perché non ho aggiunto la variabile ErrMsg, invece ho semplicemente inserito tutto ciò che la variabile avrebbe contenuto in Synchronize, che NON avrebbe funzionato, comunque Non ho idea del perché.Me ne sono reso conto quando non avevo altre idee, e ho solo pasticciato con le soluzioni.
Come sempre, lo scherzo è su di me.=P
Soluzione
Ecco la mia "presa" molto breve sulla questione. Funziona solo su Delphi 2010+ (perché quella versione ha introdotto metodi anonimi). A differenza dei metodi più sofisticati già pubblicati, il mio mostra solo il messaggio di errore, niente di più, niente di meno.
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;
Altri suggerimenti
Qualcosa di molto importante che devi capire sullo sviluppo multi-thread:
Ogni thread ha il proprio stack di chiamate, Quasi come se fossero programmi separati.Ciò include il thread principale del tuo programma.
I thread possono interagire tra loro solo in modi specifici:
- Possono operare su dati o oggetti condivisi. Ciò può portare a "condizioni di gara" di problemi di concorrenza e pertanto è necessario essere in grado di aiutarli a "condividere bene i dati".Il che ci porta al punto successivo.
- Possono "segnalarsi a vicenda" utilizzando una varietà di routine di supporto del sistema operativo.Questi includono cose come:
- Mutex
- Sezioni critiche
- Eventi
- E infine puoi inviare messaggi ad altri thread. A condizione che il thread sia stato in qualche modo scritto per essere un destinatario del messaggio.
NB:Tieni presente che i thread non possono in senso stretto chiamare direttamente altri thread.Se ad esempio il thread A provasse a chiamare direttamente il thread B, sarebbe un passaggio nello stack di chiamate del thread A!
Questo ci porta all'argomento della domanda: "non vengono sollevate eccezioni nei miei thread"
La ragione di ciò è che tutto ciò che fa un'eccezione è:
- Registra l'errore
- E rilassare il stack di chiamate.<--NB:La tua istanza TThread non può svolgere lo stack di chiamate del thread principale e non può interrompere arbitrariamente l'esecuzione del thread principale.
Quindi TThread non lo farà automaticamente segnala eccezioni alla tua applicazione principale.
Devi prendere la decisione esplicita su come desideri gestire gli errori nei thread e implementarli di conseguenza.
Soluzione
- Il primo passaggio è lo stesso di un'applicazione con thread singolo.È necessario decidere quale sia l'errore significa e come dovrebbe reagire il thread.
- Il thread deve continuare l'elaborazione?
- Il thread dovrebbe interrompersi?
- L'errore deve essere registrato/segnalato?
- L'errore richiede una decisione dell'utente?<-- Questo è di gran lunga il più difficile da implementare, quindi per ora lo salteremo.
- Una volta deciso ciò, implementare il gestore delle eccezioni appropriato.
TIP: Make sure the exception doesn't escape the thread. The OS won't like you if it does.
- Se hai bisogno che il programma principale (thread) segnali l'errore all'utente, hai alcune opzioni.
- Se il thread è stato scritto per restituire un oggetto risultato, allora è semplice:Apporta una modifica in modo che possa restituire l'errore in quell'oggetto se qualcosa è andato storto.
- Invia un messaggio al thread principale per segnalare l'errore.Tieni presente che il thread principale implementa già un ciclo di messaggi, quindi la tua applicazione segnalerà l'errore non appena elaborerà quel messaggio.
MODIFICARE:Esempio di codice per il requisito indicato.
Se tutto ciò che vuoi fare è avvisare l'utente, allora La risposta di Cosmind Prunddovrebbe funzionare perfettamente per Delphi 2010.Le versioni precedenti di Delphi necessitano di un po' più di lavoro.Quanto segue è concettualmente simile a La risposta di Jeff, ma senza gli errori:
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;
Alcune importanti correzioni sulla risposta di Jeff, inclusa l'implementazione mostrata nella sua domanda:
La chiamata a Terminate
è rilevante solo se il tuo thread è implementato all'interno di a while not Terminated do
...ciclo continuo.Dai un'occhiata a cosa Terminate
il metodo effettivamente lo fa.
La chiamata a Exit
è uno spreco inutile, ma probabilmente lo hai fatto a causa del tuo prossimo errore.
Nella tua domanda, stai racchiudendo ogni passaggio a sé stante try...except
per gestire l'eccezione.Questo è un no-no assoluto!In questo modo fingi che, anche se si è verificata un'eccezione, tutto sia ok.Il tuo thread tenta il passaggio successivo, ma in realtà è garantito che fallirà!Questo è non il modo di gestire le eccezioni!
I thread non propagano automaticamente le eccezioni in altri thread. Quindi devi affrontarlo da solo.
Rafael ha delineato un approccio, ma ci sono alternative. La soluzione Rafael punta a trattare l'eccezione in modo sincrono maturandola nel thread principale.
In uno dei miei usi del threading, un pool di thread, i thread catturano e assumono la proprietà delle eccezioni. Ciò consente al thread di controllo di gestirli man mano che piace.
Il codice sembra così.
procedure TMyThread.Execute;
begin
Try
DoStuff;
Except
on Exception do begin
FExceptAddr := ExceptAddr;
FException := AcquireExceptionObject;
//FBugReport := GetBugReportCallStackEtcFromMadExceptOrSimilar.
end;
End;
end;
Se il thread di controllo sceglie di aumentare l'eccezione, può farlo in questo modo:
raise Thread.FException at Thread.FExceptAddr;
A volte potresti avere un codice che non può chiamare la sincronizzazione, ad esempio alcune DLL e questo approccio è utile.
Nota che se non si aumenta l'eccezione catturata, è necessario distruggere altrimenti si ha una perdita di memoria.
Bene,
Sarà difficile senza il tuo codice sorgente, ma ho testato questo:
Come gestire le eccezioni negli oggetti Tthread
E funziona bene. Forse dovresti dare un'occhiata.
MODIFICARE:
Non stai seguendo ciò che i link che indichi ci dicono di fare. Controlla il mio link e vedrai come farlo.
EDIT 2:
Provalo e dimmi se ha funzionato:
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;
EDIT 3:
Hai detto che non riesci a catturare eidhttpprotocolexception. Ma funziona per me. Prova questo campione e guardalo da solo:
procedure TUpdaterThread.Execute;
begin
Try
raise EIdHTTPProtocolException.Create('test');
Except
HandleException;
End;
end;
In precedenza ho usato SendMessge per la comunicazione tra thread usando TWMCopyData, quindi penso che il seguente dovrebbe funzionare:
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;
L'ho usato solo per l'invio di semplici tipi di dati o stringhe, ma sono sicuro che potrebbe essere adattato a inviare maggiori informazioni, se necessario.
Avrai bisogno di Aggiungi Self.Handle
al costruttore nella forma ha creato il thread e gestire il messaggio nella forma che lo ha creato
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;
Strano che tutti abbiano risposto a questa domanda, ma non sono riusciti a individuare l'ovvio problema: dato che le eccezioni sollevate in un thread di fondo sono asincroni e possono verificarsi in qualsiasi momento, ciò significa che mostrare le eccezioni da un thread di fondo aprirebbe una finestra di dialogo a caso Tempi all'utente, molto probabilmente mostrando un'eccezione che non ha nulla a che fare con ciò che l'utente sta facendo al momento. Dubito che farlo potrebbe migliorare l'esperienza dell'utente.