Marshalling UI Updates in Delphi: Synchronize vs Queue in Practice
When a Delphi worker thread touches UI controls you’ll get intermittent crashes. Use TThread.Queue for fire‑and‑forget updates and reserve TThread.Synchronize for blocking calls. This guide shows a concrete example, trade‑offs, and how to verify the RTL behaviour.
08 Oct 2026, 15:20 UTC

Problem: UI thread only, background threads break the VCL/FMX
In Delphi the VCL and FireMonkey libraries are not thread‑safe. If a worker thread touches a control directly you’ll see timing‑dependent crashes – access violations, corrupted painting, or hangs – that are hard to reproduce. The fix is to marshal the UI work back to the main thread.
How the RTL does it: TThread.Synchronize and TThread.Queue
Both TThread.Synchronize and TThread.Queue rely on the main thread’s message loop. They differ in blocking behaviour and in how they are called.
- Synchronize – blocks the calling worker until the supplied method runs on the main thread. It guarantees that the UI update finishes before the worker continues. The worker is paused, so throughput drops and a deadlock can happen if the main thread is already waiting for the worker.
- Queue – posts the request and returns immediately. The queued method is executed later when the main thread processes its message queue. The worker keeps running, but the UI update may be delayed by other queued items.
Both overloads accept an anonymous method: TThread.Queue(nil, procedure begin … end). Passing nil targets the main thread. Anonymous methods capture variables by reference, so if you mutate the variable after queuing you must copy it to a local.
Concrete Example: Updating a ProgressBar from a Worker
Suppose a background thread calculates the percentage of a long operation and wants to update a TProgressBar. Using Queue keeps the worker free to keep computing.
procedure TForm1.StartWork;
begin
TThread.CreateAnonymousThread(
procedure
var
pct: Integer;
begin
while not Done do
begin
// heavy calculation
pct := CalculateProgress;
// copy to avoid race
TThread.Queue(nil,
procedure
begin
ProgressBar.Position := pct;
end);
Sleep(50); // simulate work
end;
end).Start;
end;
Key points:
- The
pctvariable is copied into the anonymous method’s scope before queuing. - The worker never blocks; it can keep producing updates.
- When the form is closing, the queued closure may run after the form is destroyed – guard it with a validity check or cancel pending work.
Trade‑offs, Limitations, and Verification
Use Queue when you:
- Need a fire‑and‑forget UI update (progress bars, status messages).
- Want to avoid blocking the worker.
- Can tolerate a small delay before the UI reflects the change.
Reserve Synchronize for:
- When the worker must wait for a result from the UI.
- Strict ordering is required.
- You need to query a UI control’s state before continuing.
Beware that:
- Both methods require a running message loop. If called before
Application.Initializeor after the main loop stops, the queued work is lost. - In older Delphi releases
TThread.ForceQueuemay not exist; check your RTL source for available overloads. - Calling
Synchronizefrom the main thread can deadlock in older RTL versions; newer releases guard against it. - Queued closures can outlive the object that captured them – guard with
if Assigned(Self) and not Self.IsFormClosing then …or cancel on shutdown.
How to check:
- Open
System.Classes.pasin your Delphi installation and read the implementations ofTThread.Synchronize,TThread.Queue, andTThread.ForceQueueto confirm blocking vs posting semantics. - Build a small test: spawn a thread that queues 1000 progress updates while a button keeps the UI responsive. Verify that the UI stays live and all updates appear.
- Close the form while work is still queued and watch for access violations or missing updates. Add a guard to the anonymous method to avoid crashes.
- To test capture semantics, mutate a variable after queuing and print its value inside the queued method.
Conclusion: Pick the Right Tool for the Job
In Delphi, TThread.Queue is the default choice for UI updates from background threads: it’s non‑blocking, easy to use with anonymous methods, and scales well. Reserve TThread.Synchronize for cases that truly need the worker to wait. Always guard queued closures against object lifetime issues and verify the RTL behaviour on your target Delphi version. With these practices you’ll eliminate the flaky UI bugs that plague multithreaded Delphi applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.