Delphi: when calling TThread.Synchronize, ensure the synchronised method handles exceptions
Posted by jpluimers on 2019/09/17
Since about a decade, TThread
has a few overloaded [WayBack] Synchronize
methods which all allow some specified synchronised method to run in the context of the main thread:
- [WayBack] TThread.Synchronize Method (TThreadMethod)
- [WayBack] TThread.Synchronize Method (TThreadProcedure)
- [WayBack] TThread.Synchronize Method (TThread, TThreadMethod)
- [WayBack] TThread.Synchronize Method (TThread, TThreadProcedure)
Any exceptions raised in that methods are caught using [WayBack] System.AcquireExceptionObject and re-raised in the calling thread.
If that happens, you loose a piece of the stack information. I knew that, but found out the hard way that it does because I had to hunt for bugs through inherited code written by people that did not know.
This was part of the stack trace that code would show during an exception:
Exception EAccessViolation at $004D732F: Access violation at address $00409174 in module ''.....exe''.
Read of address 80808080
StackTrace:
(000D632F){.....exe} [004D732F] System.Classes.TThread.Synchronize$qqrp41System.Classes.TThread.TSynchronizeRecordo (Line 14975, "System.Classes.pas" + 40) + $0
(000D6430){.....exe} [004D7430] System.Classes.TThread.Synchronize$qqrxp22System.Classes.TThreadynpqqrv$v (Line 15007, "System.Classes.pas" + 9) + $A
(005D6E61){.....exe} [009D7E61] IdSync.DoThreadSync$qqrp18Idthread.TIdThreadynpqqrv$v (Line 281, "IdSync.pas" + 21) + $6
(005D6E87){.....exe} [009D7E87] IdSync.TIdSync.SynchronizeMethod$qqrynpqqrv$v (Line 326, "IdSync.pas" + 2) + $8
Exception EAccessViolation at $00409174: Access violation at address $00409174 in module ''.....exe''. Read of address $80808080 with StackTrace
(00008174){.....exe} [00409174] System.@IsClass$qqrxp14System.TObjectp17System.TMetaClass + $C
The first exception has a different address than the one in the exception message.
Which means that you miss the whole stack path to the _IsClass
call (the underlying method implementing the as
keyword) that the actual exception was initiated at.
And yes: the $80808080
is the FastMM4
marker for freed memory, so this was a use-after-free
scenario.
A simple wrapper like this using a central logging facility gave much more insight in the actual cause:
procedure RunLoggedMethod(AMethod: TMethod); begin try AMethod(); except on E: Exception do begin Logger.LogExceptionDuringMethod(E, AMethod); raise; // mandatory to stay compatible with the old un-logged code end; end; end;
Then call it like this inside a thread descendant:
Synchronize(RunLoggedMethod(MethodToRunInMainThread));
The old code was like this:
Synchronize(MethodToRunInMainThread);
This was quite easy to change, as I already had boiler code around exported DLL functions that had a similar construct (without the raise;
as exceptions cannot pass DLL boundaries unless very specific circumstances hold).
Similar methods are needed to encapsulate procedure TIdSync.Synchronize()
, procedure TIdSync.SynchronizeMethod
, procedure TIdThread.Synchronize(Method: TThreadMethod)
and [WayBack] Queue
overloads:
- [WayBack] TThread.Queue Method (TThreadMethod)
- [WayBack] TThread.Queue Method (TThreadProcedure)
- [WayBack] TThread.Queue Method (TThread, TThreadMethod)
- [WayBack] TThread.Queue Method (TThread, TThreadProcedure)
–jeroen
Remy said
Synchronize(RunLoggedMethod(MethodToRunInMainThread));
This does not compile, as it calls RunLoggedMethod() first without syncing, and then tries to call Synchronize() with the result of the previous call, which there is no result. You can’t pass parameters to the procedure that is passed to Synchronize(). Perhaps you meant to call Synchronize() with an anonymous procedure instead?
Synchronize(
procedure
begin
RunLoggedMethod(MethodToRunInMainThread);
end
);