The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 4,224 other subscribers

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

http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Tutorial:_Using_Tasks_from_the_Parallel_Programming_Library

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; as procedure in TThread.Synchronize ?

Why not a simple Label1.Text := lValue.ToString; ?

Basically that question can be either of these:

  1. Why is there no easier language construct than wrapping the callback in a procedure (anonymous method).
  2. 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 to TComponent 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:

  1. Why is there no easier language construct than wrapping the callback in a procedure (anonymous method).
  2. 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:

If your question is “why do I need TThread.Synchronize at all” then the answer is that Label1.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 to TThread.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 the lValue.ToString() result into a local variable first so it can be captured.

Explanations and of 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:

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:

  1. the capturing takes place when the anonymous method is created
  2. when the anonymous method is executed, it will see the state at the time of capture
  3. 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 and reference to function

For the latter, see for instance: [WayBack] Anonymous Methods in Delphi and [WayBack] Classes.TThreadProcedure Type.

More topics on capturing are at:

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:

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:

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:

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://…


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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

 
%d bloggers like this: