Delphi multi-threading: confused by TThread.Synchronize / TThread.Queue? You’re not alone. And you need to be aware of exceptions there too.
Posted by jpluimers on 2020/01/01
Below an elaboration on my answer to the question [WayBack] I don’t understand the following part of the second Delphi example:TThread.Synchronize… – Alberto Paganini – Google+:
I was looking at the Task example at the EMB wiki link below
and I don’t understand the following part of the second Delphi example:
TThread.Synchronize(nil, procedure begin Label1.Text := lValue.ToString; end);why is there the need to pass
Label1.Text := lValue.ToString;
asprocedure
inTThread.Synchronize
?Why not a simple
Label1.Text := lValue.ToString;
?
Basically that question can be either of these:
- Why is there no easier language construct than wrapping the callback in a
procedure
 (anonymous method). - Why a call toÂ
TThread.Synchronize
 at all?
The tutorial that Alberto refers to is [WayBack] Tutorial: Using Tasks from the Parallel Programming Library – RAD Studio. That example uses the TTask
 feature in Delphi, but the portion he has a question about is general to any multi-treading in Delphi that touches the updating of a VCL or FMX UI from another thread.
This is the more complete code in the meant example:
procedure TForm1.ButtonTask1Click(Sender: TObject); var lValue: Integer; begin Label1.Text := '--'; TTask.Run(procedure begin {Some calculation that takes time} Sleep(3000); lValue := Random(10); TThread.Synchronize(nil, procedure begin Label1.Text := lValue.ToString; end); end); end;
What happens here is that the TTask
 is used to run some code (starting with {Some calculation that takes time}
) in a different thread (that is being determined by the framework behind TTask
).
I recommend against using
TTask
 (or any other part of the Delphi Parallel Programming Library) as I agree with Stefan Glienke in [WayBack] Hello, Do you know why the “default” keyword of a class property is sometime defined also as an attribute ? – Paul TOTH – Google+:If that works as well as the PPL does I would not touch it with a 10 foot pole ;)
With “that”, he refers to the APL (Asynchronous Programming Library [Archive.is]) which got introduced in Delphi XE8 and adds asynchronous support for UI controls, but unlike .NET (which implements it on the .NET
TControl
equivalent) shifted it down toTComponent
 probably because that’s the common ancestor for both VCL and FMX. But that’s a topic for another day.The Delphi Parallel Programming Library (DPL) is a complex and intricate framework originally started as Delphi Parallel Library (DPL) and modelled after [WayBack] Intel’s Threading Building Blocks (TBB) and Microsoft’s Task Parallel Library (TPL) (not in WayBack, but moved to the CHM file [WayBack], see also[WayBack] Task Parallel Library changes since the MSDN Magazine article | Parallel Programming with .NET)
Most parts of the DPL were written over at least 7 years time by former Chief Scientist Allen Bauer and introduced in Delphi XE7 as the Delphi Parallel Programming Library (PPL), see [WayBack] Delphi Parallel Programming Library & Memory Managers – Steve Maughan and [WayBack] The Oracle at Delphi: Lock my Object… Please!.
Since Delphi XE7, the PPL hardly got maintenance and now most if not all of the people having knowledge about it have left Embarcadero: early 2016, [Archive.is] Delphi Chief Scientist Allen Bauer Has Left Embarcadero/Idera | Hacker News leaving a big gap as “There are no plans that I’m aware of to move someone into my old position… All those that I would consider qualified are either already gone or are currently looking elsewhere.” (it is not in the [WayBack] part of How safe is Delphis future? Who is the new Lead Compiler Engineer? Who is the new Chief Scientist? – Ralf Stocker – Google+, hence the screen shot).
Luckily, Allen copied most of his old Embarcadero blog over to his personal one (including comments!), so there is quite some historic reference, see for instance [WayBack] Thread pools <> Task handling. – Community Blogs – Embarcadero Community and [WayBack] The Oracle at Delphi: Thread pools <> Task handling.
So back ensuring you can execute some code on the main thread and the questions from the G+ post:
- Why is there no easier language construct than wrapping the callback in a
procedure
 (anonymous method). - Why a call toÂ
TThread.Synchronize
 at all?
Lets start with the questions in order I answered them, starting with the my response:
If your question is “why can’t you do either of these:
TThread.Synchronize(nil, () => Label1.Text := lValue.ToString);TThread.Synchronize(nil, begin Label1.Text := lValue.ToString; end);TThread.Synchronize(nil, Label1.Text := lValue.ToString);then the answer is “The Delphi language does not support lambda expressions, only anonymous methods”.
For an explanation of those, see these links:
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-use-lambda-expressions-in-a-query
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-use-lambda-expressions-outside-linq
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-methods
If your question is “why do I need
TThread.Synchronize
at all” then the answer is thatLabel1.Text := lValue.ToString
needs to be executed on the UI thread (usually the main thread) because neither the VCL nor the FMX UI thread is thread-safe.If you have to wait for that assignment to be processed, then you need to call
TThread.Synchronize
. If you do not have to wait, then a call toTThread.Queue
would suffice. It would look like this:TThread.Queue(nil, procedure begin Label1.Text := lValue.ToString; end);Here I’ve assumed that
lValue
is a local variable (i.e. stack based and can be captured) to your outer method and cannot be overwritten by another thread. If that is not true, then you need to get thelValue.ToString()
result into a local variable first so it can be captured.Explanations and of Queue:
- https://stackoverflow.com/questions/42280937/delphi-queue-and-synchronize
- http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TThread.Queue
The
Queue
call will execute the anonymous method asynchronously and not wait for the completion. It basically encapsulates the thread-safe queue that +Lars Fosdal was talking about.More on the capturing of variables:
- https://stackoverflow.com/questions/5154914/how-and-when-are-variables-referenced-in-delphis-anonymous-methods-captured/5154920
- http://interactiveasp.net/blogs/spgilmore/archive/2010/06/17/anonymous-methods-and-closures-in-delphi-2010.aspx
- https://stackoverflow.com/questions/31193949/is-it-possible-to-capture-the-values-of-variables-local-to-a-delphi-anonymous-me
- http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/devcommon/anonymousmethods_xml.html
More in depth information on the language
The last big Delphi version in which large language additions were launched AND later stabilised was Delphi 2009: it introduced support for Unicode, generics and anonymous methods all at the same time, see for instance [WayBack] Anonymous methods and Generics in Delphi 2009 – Hadi Hariri. That was a lot (actually: too much) for that new version, but also long overdue. The most important issues got fleshed out over the next three or four Delphi versions with smaller things still being fleshed out.
Windows had been supporting Unicode partially on [WayBack] Unicode in Win32s and Windows 95 and fully on [WayBack] Unicode on Windows NT so with support in Win32s (1994) and Windows NT (1993), support for it lage 2008 (when Delphi 2009 was launched [WayBack]) was long overdue.
Generics is completely out of this blog post as it has nothing to do with the question. But it still is a complex beast, just look at the few libraries in Delphi (Spring4D the most important one) taking fully advantage of it.
Anonymous methods
The anonymous methods in Delphi (often called Anonymous Functions) together with the support of capturing variables, implement the concept of Closure (computer programming) – Wikipedia.
Capturing variables is a complex topic, so I will only give a very shallow example to show similarity with local functions having access to variables in outer functions:
procedure TLoggedThreadSynchronizeHelper.LoggedSynchronize(const aContext: string; const aMethod: TThreadMethod); begin try Synchronize( procedure() begin try aMethod(); except on E: Exception do begin _LogException(aContext, E); raise; end; end; end); except on E: Exception do begin _LogException(aContext, E); raise; end; end; end;
What you see here is that the outer parameters aContext
 and aMethod
 can still be used inside the anonymous method star starts with procedure
, this despite Synchronize
 ensuring that procedure gets called on te main thread (which can be completely different).
More on Synchronize
 later on, as the important aspect is the capture
 aspect.
Since the anonymous method can use aContext
 and aMethod
 long after LoggedSynchronize
 has finished (and  aContext
 and aMethod
 went out of scope), the compiler needs to capture  aContext
 and aMethod
 and pass it on to the anonymous procedure.
The mechanism of how the compiler does this is not important, but you have to remember a few things:
- the capturing takes place when the anonymous method is created
- when the anonymous method is executed, it will see the state at the time of capture
- since a capture can take place, references to anonymous methods are slightly different than the already existing [WayBack] Procedural Types, so now you have
reference to procedure
 andreference to function
For the latter, see for instance: [WayBack] Anonymous Methods in Delphi and [WayBack] Classes.TThreadProcedure Type.
More topics on capturing are at:
- [WayBack] How and when are variables referenced in Delphi’s anonymous methods captured? – Stack Overflow
- [WayBack] Anonymous Methods and Closures in Delphi 2010 – Phil Gilmore
- [WayBack] Is it possible to capture the values of variables local to a Delphi anonymous method/closure? – Stack Overflow
- [WayBack] Anonymous Methods in Delphi
No lambda expressions
Anonymous methods are not lambda expressions: they are full methods with parameters and such. I don’t think this will be implemented in the Delphi language anytime soon and if it is, requires at least four full Delphi versions to become really stable.
So for now, we will miss these in Delphi:
- [WayBack] Lambda Expressions (C# Programming Guide) | Microsoft Docs
- [WayBack] How to: Use Lambda Expressions in a Query (C# Programming Guide) | Microsoft Docs
- [WayBack] How to: Use Lambda Expressions Outside LINQ (C# Programming Guide) | Microsoft Docs
- [WayBack] Anonymous Methods (C# Programming Guide) | Microsoft Docs
- [WayBack] Evolution of the Oxygene Language | Oxygene | Elements
I’ve included the last two links so you can compare lambda expressions and anonymous methods in C# and Oxygene (very similar to the Object Pascal language) so it is easier to get an impression how they could differ in a future Object Pascal.
Some more background information on threading
Before reading any further, remember that threading is hard.
Even when frameworks makes it easier to manage threads, so you’re making less “code ceremony” mistakes, the concept is still hard to manage.
Important aspects to be aware of when doing multi-threading are:
- timing aspects (since things run in parallel, you need to be aware of the what and when of things to be executed and their interdependencies)
- data sharing and protection aspects (when more than one thread can access the same data, be aware of what consequences that has)
- limitations of your libraries, for instance UI libraries like VCL or FMX can only alter UI state from within the state where the UI object has been created. Similarly, many database or other communications layer limit operations on objects to the thread that object was initialised on.
I’ll continue with UI aspects, as that’s what the example was about.
Especially for UI centric applications like VCL or FMX, where the only safe way to update the UI is from the UI thread which – by default, and I’ve never seen otherwise – is the main thread.
The code is much older (some parts as far back as Delphi 2, others towards of the Delphi non-Unicode era), but the earliest on-line viewable documentation for this is in Delphi 2007:
- [WayBack] TThread Class (introduced in Delphi 2)
- [WayBack] Queue Method (introduced in Delphi 2006)
- [WayBack] TThread.Queue Method (TThreadMethod)Â (introduced in Delphi 2006)
- [WayBack] TThread.Queue Method (TThread, TThreadMethod)Â (introduced in Delphi 2006)
- [WayBack] Synchronize Method (introduced in Delphi 2)
- [WayBack] TThread.Synchronize Method (TThreadMethod)Â (introduced in Delphi 2)
- [WayBack] TThread.Synchronize Method (TThread, TThreadMethod)Â (introduced in Delphi 7)
- [WayBack] Using the Main VCL Thread
So what sets Synchronize and Queue apart?
Somehow, many Delphi people seem learn about Synchronize
 first, but then never learn about Queue
. Both will enqueue the execution of the target method on the main thread, after which Synchronize
 will wait for completion of that method, while Queue
returns back immediately. This is explained in more detail at [WayBack]multithreading – Delphi Queue and Synchronize – Stack Overflow
So if you do not need to wait, Queue
 usually will give much better performance than Synchronize
, especially if multiple threads call Synchronize
 at the same time (they will be handled one by one, while the other threads just wait).
The risk of queueing without waiting is that you queue updates for the same UI controls multiple times. This is where these methods can come in usefull:
- [WayBack] TThread.RemoveQueuedEvents Method:
class procedure RemoveQueuedEvents(AThread: TThread; AMethod: TThreadMethod);
If you organise your code so each element gets only updated from one TThreadMethod
, then you can encapsulate a call to RemoveQueuedEvents
 and a corresponding Queued
in a central place.
The thread at [WayBack] I don’t understand the following part of the second Delphi example:TThread.Synchronize… – Alberto Paganini – Google+ has a few more tips on this.
What if anything goes wrong?
Errors in multi-threading are like errors in DLL boundaries: if they cross a thread or DLL boundary, then you are usually in trouble.
For DLL boundaries, this is usually because on both sides you have different tooling, memory models, or Run-Time Library instances.
With threading, you have different stacks, which is especially important with Synchronize
, but also important for Queue
.
When an exception occurs in the method that was scheduled in the main thread, then it will happen using the stack of the main thread as well. The stack will go up all the way from where the that method was being called from, in this case the method queuing mechanism of the RTL.
When that method was scheduled by a call to Synchronize
, then the RTL will catch the exception, then re-raise it on the thread that called Synchronize. The stack now has changed back to the thread calling Synchronize, which means you just lost the full call stack in the called method.
So it makes sense to either log exceptions in all methods being queued, or have a central mechanism that does just that.
I’ve written a poor-mans central mechanism for that in a unit called LoggedExecuteInMainThreadHelpersUnit
 which is below.
It has a central type and var where you can inject your logging framework:
type TLogException = reference to procedure(const aContext: string; const aException: Exception); var LogException: TLogException = nil; // poor man's dependency injection
Then it has helpers for these classes making the catching and logging of exceptions easier:
TThread
TIdSync
TIdThread
The latter is needed because Indy overloads only the TThreadMethod
version, but fails to overload the TThreadProcedure
version (at least in Delphi XE8 which is where I wrote this for);
unit IdThread; interface ... type TIdThread = class(TThread) ... public procedure Synchronize(Method: TThreadMethod); overload; end; ... implementation ... procedure TIdThread.Synchronize(Method: TThreadMethod); begin inherited Synchronize(Method); end; ... end.
Finally, there is this little class that shows you how you could do a capture in your own code to capture both aContext
 and aMethod
:
type TThreadMethodCapture = class strict private fContext: string; fMethod: TThreadMethod; public constructor Create(const aContext: string; const aMethod: TThreadMethod); procedure ExecuteMethod; property Context: string read fContext; property Method: TThreadMethod read fMethod; end; constructor TThreadMethodCapture.Create(const aContext: string; const aMethod: TThreadMethod); begin inherited Create; fContext := aContext; fMethod := aMethod; end;
You can call it like this, passing the outer aContext
 and aMethod
 so you have them available inside the UI thread where ExecuteMethod
 uses them to log the exception if it catches one:
class procedure TIdSyncHelper.LoggedSynchronizeMethod(const aContext: string; const aMethod: TThreadMethod); var lThreadMethodCapture: TThreadMethodCapture; begin try lThreadMethodCapture := TThreadMethodCapture.Create(aContext, aMethod); try SynchronizeMethod(lThreadMethodCapture.ExecuteMethod); finally lThreadMethodCapture.Free(); end; except on E: Exception do begin _LogException(aContext, E); raise; end; end; SynchronizeMethod(aMethod); end;
–jeroen
Source: Hello, I was looking at the Task example at the EMB wiki link below http://…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
unit LoggedExecuteInMainThreadHelpersUnit; | |
interface | |
uses | |
System.Classes, | |
System.SysUtils, | |
IdSync, | |
IdThread; | |
// Allows logging of both the main thread side and invoking thread side of Synchronize/Queue calls for TThread, TIdSync and TIdThread | |
// This unit contains lots of try…except duplications, but that was a deliberate choice: it makes the resulting stack trace easier for LogException to track down for the overload used. | |
type | |
TLoggedThreadSynchronizeHelper = class helper for TThread | |
protected | |
procedure LoggedSynchronize(const aContext: string; const aMethod: TThreadMethod); overload; | |
procedure LoggedSynchronize(const aContext: string; const aThreadProc: TThreadProcedure); overload; | |
public | |
class procedure LoggedQueue(const aContext: string; const aThread: TThread; const aMethod: TThreadMethod); overload; static; | |
class procedure LoggedQueue(const aContext: string; const aThread: TThread; const aThreadProc: TThreadProcedure); overload; static; | |
class procedure LoggedSynchronize(const aContext: string; const aThread: TThread; const aMethod: TThreadMethod); overload; static; | |
class procedure LoggedSynchronize(const aContext: string; const aThread: TThread; const aThreadProc: TThreadProcedure); overload; static; | |
end; | |
TIdSyncHelper = class helper for TIdSync | |
public | |
procedure LoggedSynchronize(const aContext: string); overload; | |
class procedure LoggedSynchronizeMethod(const aContext: string; const aMethod: TThreadMethod); | |
end; | |
TIdThreadHelper = class helper (TLoggedThreadSynchronizeHelper) for TIdThread | |
public | |
procedure LoggedSynchronize(const aContext: string; const aMethod: TThreadMethod); overload; | |
end; | |
TLogException = reference to procedure(const aContext: string; const aException: Exception); | |
var | |
LogException: TLogException = nil; // poor man's dependency injection | |
implementation | |
procedure _LogException(const aContext: string; const aException: Exception); | |
var | |
lLogException: TLogException; | |
begin | |
try | |
lLogException := LogException; | |
if Assigned(lLogException) then | |
lLogException(aContext, aException); | |
except | |
; // NOP: eat all exceptions. Not the nicest solution, but: if logging exceptions breaks, how to log another exception? | |
end; | |
end; | |
// TThreadMethodCapture | |
type | |
TThreadMethodCapture = class | |
strict private | |
fContext: string; | |
fMethod: TThreadMethod; | |
public | |
constructor Create(const aContext: string; const aMethod: TThreadMethod); | |
procedure ExecuteMethod; | |
property Context: string read fContext; | |
property Method: TThreadMethod read fMethod; | |
end; | |
constructor TThreadMethodCapture.Create(const aContext: string; const aMethod: TThreadMethod); | |
begin | |
inherited Create; | |
fContext := aContext; | |
fMethod := aMethod; | |
end; | |
procedure TThreadMethodCapture.ExecuteMethod; | |
begin | |
try | |
Method(); | |
except | |
on E: Exception do | |
begin | |
_LogException(Context, E); | |
raise; | |
end; | |
end; | |
end; | |
// TLoggedThreadSynchronizeHelper | |
procedure TLoggedThreadSynchronizeHelper.LoggedSynchronize(const aContext: string; const aMethod: TThreadMethod); | |
begin | |
try | |
Synchronize( | |
procedure() | |
begin | |
try | |
aMethod(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
procedure TLoggedThreadSynchronizeHelper.LoggedSynchronize(const aContext: string; const aThreadProc: TThreadProcedure); | |
begin | |
try | |
Synchronize( | |
procedure() | |
begin | |
try | |
aThreadProc(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
class procedure TLoggedThreadSynchronizeHelper.LoggedQueue(const aContext: string; const aThread: TThread; const aMethod: TThreadMethod); | |
begin | |
try | |
Queue(aThread, | |
procedure() | |
begin | |
try | |
aMethod(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
class procedure TLoggedThreadSynchronizeHelper.LoggedQueue(const aContext: string; const aThread: TThread; const aThreadProc: TThreadProcedure); | |
begin | |
try | |
Queue(aThread, | |
procedure() | |
begin | |
try | |
aThreadProc(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
class procedure TLoggedThreadSynchronizeHelper.LoggedSynchronize(const aContext: string; const aThread: TThread; const aMethod: TThreadMethod); | |
begin | |
try | |
Synchronize(aThread, | |
procedure() | |
begin | |
try | |
aMethod(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
class procedure TLoggedThreadSynchronizeHelper.LoggedSynchronize(const aContext: string; const aThread: TThread; const aThreadProc: TThreadProcedure); | |
begin | |
try | |
Synchronize(aThread, | |
procedure() | |
begin | |
try | |
aThreadProc(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
// TIdSyncHelper | |
procedure TIdSyncHelper.LoggedSynchronize(const aContext: string); | |
begin | |
try | |
Synchronize(); | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
class procedure TIdSyncHelper.LoggedSynchronizeMethod(const aContext: string; const aMethod: TThreadMethod); | |
var | |
lThreadMethodCapture: TThreadMethodCapture; | |
begin | |
try | |
lThreadMethodCapture := TThreadMethodCapture.Create(aContext, aMethod); | |
try | |
SynchronizeMethod(lThreadMethodCapture.ExecuteMethod); | |
finally | |
lThreadMethodCapture.Free(); | |
end; | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
SynchronizeMethod(aMethod); | |
end; | |
// TIdThreadHelper | |
procedure TIdThreadHelper.LoggedSynchronize(const aContext: string; const aMethod: TThreadMethod); | |
var | |
lThreadMethodCapture: TThreadMethodCapture; | |
begin | |
try | |
lThreadMethodCapture := TThreadMethodCapture.Create(aContext, aMethod); | |
try | |
Synchronize(lThreadMethodCapture.ExecuteMethod); | |
finally | |
lThreadMethodCapture.Free(); | |
end; | |
except | |
on E: Exception do | |
begin | |
_LogException(aContext, E); | |
raise; | |
end; | |
end; | |
end; | |
end. |
f
Leave a Reply