Move Long‑Running Work Off the VCL Main Thread Using Delphi’s TTask and TThread.Queue
Learn how to keep a VCL/FMX form responsive by offloading blocking work to TTask and marshaling UI updates safely with TThread.Queue.
01 Jan 2026, 08:59 UTC

Desired outcome
You have a VCL (or FMX) form that performs a blocking operation—such as a file copy, a network request, or a heavy calculation—when the user clicks a button. The goal is to keep the user interface responsive while the work runs in the background and to update controls safely when the task finishes or reports progress.
Prerequisites
- Delphi XE7 or newer (the
System.Threadingunit was introduced in XE7). - A VCL or FMX project with a form that contains at least one visual control you want to update (e.g., a
TLabelfor status, aTProgressBar, or afor log output). - Basic familiarity with Object Pascal syntax and anonymous methods.
Procedure
-
Add the required unit
In the uses clause of your form unit, add
System.Threading:uses System.SysUtils, System.Classes, Vcl.Controls, Vcl.StdCtrls, Vcl.Forms, System.Threading; // <‑‑ adds TTask, TFuture, TThread.Queue etc. -
Define a cancellation flag
Long‑running tasks should check a flag periodically so they can exit cleanly. Declare a field in the form class:
type TForm1 = class(TForm) btnStart: TButton; btnCancel: TButton; lblStatus: TLabel; private FCancelRequested: Boolean; procedure StartWork; procedure CancelWork; public end; -
Implement the background work as an anonymous method
The method receives no parameters and returns nothing. All UI updates must be marshaled back to the main thread with
TThread.Queue(asynchronous) orTThread.Synchronize(blocking). UseTThread.Queuefor fire‑and‑forget updates to avoid possible deadlocks.procedure TForm1.StartWork; begin FCancelRequested := False; btnStart.Enabled := False; btnCancel.Enabled := True; // TTask.Run returns an ITask; we keep it only if we need to wait or cancel later. TTask.Run( procedure var i: Integer; workItem: string; begin try for i := 1 to 100 do begin // Check cancellation flag each iteration. if FCancelRequested then Exit; // Simulate work – replace with your real blocking code. Sleep(50); // 50 ms ≈ 0.05 s // Prepare a status string; capture i by value to avoid reference‑capture issues. workItem := Format('Processed item %d/100', [i]); // Marshal UI update to the main thread. TThread.Queue(nil, procedure begin lblStatus.Caption := workItem; // Optional: update a progress bar. // ProgressBar1.Position := i; end); end; // Final message when the loop finishes normally. TThread.Queue(nil, procedure begin lblStatus.Caption := 'Work completed.'; btnStart.Enabled := True; btnCancel.Enabled := False; end); except on E: Exception do begin // Marshal exception info to the UI thread. TThread.Queue(nil, procedure begin lblStatus.Caption := Format('Error: %s', [E.Message]); btnStart.Enabled := True; btnCancel.Enabled := False; end); end; end; end); end; -
Wire the UI controls
Connect the button click events to the methods above:
procedure TForm1.btnStartClick(Sender: TObject); begin StartWork; end; procedure TForm1.btnCancelClick(Sender: TObject); begin FCancelRequested := True; lblStatus.Caption := 'Cancelling…'; end; -
Optional: obtain a result with TFuture
If the background work produces a value you need later, use
TFuture. Read.Valueonly after checkingIsCompletedto avoid blocking the UI thread unintentionally.var FutureInt: TFuture; begin FutureInt := TTask.Run function begin // Simulate a calculation. Sleep(200); Result := 42; end; // Later, when you need the result (still on the UI thread): if FutureInt.IsCompleted then ShowMessage('Result: ' + FutureInt.Value.ToString) else ShowMessage('Still computing…'); end;
Expected checks
- While the task runs, the form remains responsive: you can move the window, click other buttons, or cancel the operation.
- Status updates appear in the label in the order they are queued.
- If you click Cancel, the flag is set, the task exits on the next loop iteration, and the label shows “Cancelling…” followed by the final state.
- If an exception occurs inside the task, the label shows the error message and the application does not crash.
Recovery options (rollback)
The procedure described does not permanently alter system state; it only creates temporary tasks. If you need to abort a task that has already started, setting the cancellation flag is the recommended cooperative approach. There is no forced termination in the RTL, so abandoning a task while freeing captured objects can cause access violations. Ensure any objects captured by the anonymous method (e.g., a stream or a data structure) remain valid until the task finishes or is cancelled.
If you discover that a task is consuming too many thread‑pool threads, consider:
- Replacing the long‑running
TTask.Runwith a dedicatedTThreadsubclass for that specific workload. - Limiting the degree of parallelism by using a custom thread pool (e.g.,
System.Threading.TParallelwith aMaxWorkerThreadssetting) if your Delphi version supports it.
Limitations and verification
The default thread pool is shared across all RTL users; flooding it with many long tasks can starve other library code. Monitor the number of active tasks in a test environment and, if necessary, move very long operations to a dedicated thread.
To verify the implementation:
- Run the application, click Start, and confirm the UI stays responsive.
- Click Cancel while the task is running and observe a clean exit.
- Introduce a
raise Exception.Create('test');inside the task body and confirm the error appears in the label without crashing the app. - Check that the
System.Threadingunit compiles and that the symbolsTTask,IFuture,TThread.Queueresolve correctly for your Delphi version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.