Asynchronous Chilkat Operations in Lazarus / Free Pascal
running Chilkat methods on a background thread with TThread
Applies to Chilkat for Lazarus / Free Pascal on Windows, Linux, and macOS.
The release notes for Chilkat for Lazarus state that the API is synchronous only: there are no
*Async method variants, and no Task or TaskChain classes. The most common
question about that note is whether it describes a transitional state. It does not.
The short answer
Chilkat for Lazarus will not add *Async, Task, or TaskChain.
This is a deliberate design decision, not a gap to be filled in a later release.
Chilkat's position going forward is that asynchronous behavior belongs in the programming language,
with Chilkat's ordinary synchronous methods called from whatever thread the language gives you. In Object
Pascal that language facility is TThread, and it is a better tool than the Chilkat task system
was: you control the thread, you control its lifetime, and progress and completion arrive on the UI thread
through the mechanism the LCL already provides.
Nothing is lost. Everything Task did — background execution, progress reporting,
cancellation, chaining, and running many operations in parallel — is a few lines of standard Pascal.
This page shows each one.
Why the async API is not being carried forward
Chilkat's *Async methods worked by running the operation on a background thread
inside Chilkat. That design has consequences that get worse, not better, over time:
- The
TaskCompletedcallback fires on Chilkat's internal thread, not on your application's main thread. In a GUI framework such as the LCL, touching a control from that callback is undefined behavior. Every correct program therefore had to marshal the result back to the main thread anyway — so the thread-hopping was never actually avoided, only hidden. - The task system was a second, parallel scheduling model that had to be learned in addition to the one your language already has. Two thread pools, two cancellation mechanisms, two ways to sequence work.
- It could not compose with anything else in your program. A Chilkat
TaskChaincould chain Chilkat calls, but it could not interleave a database write, a file rename, or a call into another library.
Doing the concurrency in Pascal removes all three problems at once. There is one thread model in your application, one place where work is cancelled, and Chilkat calls sit alongside every other call you make.
What each Chilkat task feature becomes
| Chilkat async/task feature | Lazarus / FPC equivalent |
|---|---|
obj.SomeMethodAsync(...) returning a Task |
Call obj.SomeMethod(...) inside TThread.Execute |
task.Run / task.RunOnThreadPool |
Create the TThread (or hand work to a pool of them) |
task.Wait(maxWaitMs) |
thread.WaitFor |
task.Cancel |
thread.Terminate, observed in OnAbortCheck |
task.PercentDone / TaskCompleted callback |
Chilkat's OnPercentDone event plus Queue / Synchronize |
task.GetResultString / GetResultBool |
A field on your thread class, read after WaitFor or in the completion handler |
TaskChain (step 1, then step 2, then step 3) |
Consecutive statements in one Execute method |
ThreadPool / Global.MaxThreads |
However many TThread instances you choose to create |
Five rules for calling Chilkat from a thread
- Unlock once, on the main thread, before any worker starts.
TGlobal.UnlockBundleunlocks every Chilkat class in the process for the life of the application. Do not call it from each thread. - Give each thread its own Chilkat objects. Create them in
Executeand free them inExecute. A singleTHttporTSFtpinstance must not be used by two threads at the same time. Separate instances in separate threads are fully supported — that is exactly what Chilkat's own thread pool used to do. - Remember that Chilkat events fire on the calling thread.
OnPercentDone,OnAbortCheck, andOnProgressInfoare invoked by the Chilkat method itself, so inside a worker thread they run on that worker. Never touch an LCL control from them directly. - Marshal to the UI thread with
QueueorSynchronize.Queueposts the call and returns immediately — right for frequent progress updates, since it never stalls the transfer.Synchronizeblocks the worker until the main thread has run the method — right for a final result you want delivered before the thread ends. - Cancel through
OnAbortCheck. SetHeartbeatMsso the event fires periodically, and returnTerminatedfrom the handler. ReturningTruemakes the running Chilkat method return early with a failure status.
{$mode delphi}, which is also what the
Chilkat units use. In that mode a method is assigned to an event with no operator:
http.OnPercentDone := HttpPercentDone;. If your unit uses {$mode objfpc}{$H+} instead
— the default for a new Lazarus form unit — write http.OnPercentDone := @HttpPercentDone;
with the @. Everything else is identical.
Example 1: one background operation, with progress and cancel
This is the direct replacement for calling a *Async method and waiting for
TaskCompleted. A worker thread downloads a URL with THttp.Download, reports progress to
a progress bar, and stops promptly when the user clicks Cancel.
The worker thread unit
unit DownloadThread;
{$mode delphi}{$H+}
interface
uses
Classes, SysUtils, Chilkat.Base, Chilkat.Http;
type
TDownloadProgressEvent = procedure(Sender: TObject; PctDone: Integer) of object;
TDownloadDoneEvent = procedure(Sender: TObject; Success: Boolean;
const ErrorText: string) of object;
TDownloadThread = class(TThread)
private
FUrl: string;
FLocalPath: string;
FSuccess: Boolean;
FErrorText: string;
FPctDone: Integer;
FOnProgress: TDownloadProgressEvent;
FOnDone: TDownloadDoneEvent;
// Chilkat event handlers. These run on THIS thread, not the main thread.
function HttpPercentDone(Sender: TChilkatBase; PctDone: Integer): Boolean;
function HttpAbortCheck(Sender: TChilkatBase): Boolean;
// These two run on the main thread, via Queue / Synchronize.
procedure ReportProgress;
procedure ReportDone;
protected
procedure Execute; override;
public
constructor Create(const AUrl, ALocalPath: string;
AOnProgress: TDownloadProgressEvent; AOnDone: TDownloadDoneEvent);
end;
implementation
constructor TDownloadThread.Create(const AUrl, ALocalPath: string;
AOnProgress: TDownloadProgressEvent; AOnDone: TDownloadDoneEvent);
begin
// Assign every field BEFORE calling the inherited constructor with
// CreateSuspended = False, because the thread may begin running
// the moment that constructor returns.
FUrl := AUrl;
FLocalPath := ALocalPath;
FOnProgress := AOnProgress;
FOnDone := AOnDone;
FSuccess := False;
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDownloadThread.Execute;
var
http: THttp;
begin
// Each thread creates, uses, and frees its own Chilkat objects.
http := THttp.Create;
try
if not http.IsValid then
begin
FErrorText := 'The Chilkat shared library could not be loaded.';
Synchronize(ReportDone);
Exit;
end;
http.OnPercentDone := HttpPercentDone;
http.OnAbortCheck := HttpAbortCheck;
http.HeartbeatMs := 200; // OnAbortCheck fires 5 times per second
// A perfectly ordinary synchronous Chilkat call -- it just happens
// to be running on a background thread.
FSuccess := http.Download(FUrl, FLocalPath);
if not FSuccess then
FErrorText := http.LastErrorText;
finally
http.Free;
end;
// Synchronize (not Queue) for the final notification: it is guaranteed
// to have run before this thread object is destroyed.
Synchronize(ReportDone);
end;
function TDownloadThread.HttpPercentDone(Sender: TChilkatBase;
PctDone: Integer): Boolean;
begin
FPctDone := PctDone;
Queue(ReportProgress); // does not block the download
Result := Terminated; // True aborts the transfer
end;
function TDownloadThread.HttpAbortCheck(Sender: TChilkatBase): Boolean;
begin
// Called every HeartbeatMs milliseconds, even while blocked on the
// network, so Cancel takes effect within a fraction of a second.
Result := Terminated;
end;
procedure TDownloadThread.ReportProgress;
begin
if Assigned(FOnProgress) then FOnProgress(Self, FPctDone);
end;
procedure TDownloadThread.ReportDone;
begin
if Assigned(FOnDone) then FOnDone(Self, FSuccess, FErrorText);
end;
end.
Using it from a form
unit MainForm;
{$mode delphi}{$H+}
interface
uses
Classes, SysUtils, Forms, StdCtrls, ComCtrls, DownloadThread;
type
TForm1 = class(TForm)
btnStart: TButton;
btnCancel: TButton;
ProgressBar1: TProgressBar;
lblStatus: TLabel;
procedure btnStartClick(Sender: TObject);
procedure btnCancelClick(Sender: TObject);
private
FDownload: TDownloadThread;
procedure DownloadProgress(Sender: TObject; PctDone: Integer);
procedure DownloadDone(Sender: TObject; Success: Boolean;
const ErrorText: string);
end;
implementation
{$R *.lfm}
procedure TForm1.btnStartClick(Sender: TObject);
begin
if FDownload <> nil then Exit; // one at a time
ProgressBar1.Position := 0;
lblStatus.Caption := 'Downloading...';
btnStart.Enabled := False;
btnCancel.Enabled := True;
// The form does not block. It stays responsive while this runs.
FDownload := TDownloadThread.Create(
'https://www.example.com/bigfile.zip', 'bigfile.zip',
DownloadProgress, DownloadDone);
end;
procedure TForm1.btnCancelClick(Sender: TObject);
begin
if FDownload <> nil then
FDownload.Terminate; // OnAbortCheck picks this up
end;
procedure TForm1.DownloadProgress(Sender: TObject; PctDone: Integer);
begin
// Runs on the main thread -- safe to touch controls.
ProgressBar1.Position := PctDone;
end;
procedure TForm1.DownloadDone(Sender: TObject; Success: Boolean;
const ErrorText: string);
begin
// Also on the main thread.
FDownload := nil; // FreeOnTerminate frees the object itself
btnStart.Enabled := True;
btnCancel.Enabled := False;
if Success then
lblStatus.Caption := 'Done.'
else
lblStatus.Caption := 'Failed: ' + ErrorText;
end;
end.
UnlockBundle go? In the form's OnCreate, or in the .lpr
before Application.Run — anywhere on the main thread at startup. Unlocking is process-wide and
permanent, so the worker threads inherit it automatically.
glob := TGlobal.Create;
try
if not glob.UnlockBundle('Anything for 30-day trial') then
ShowMessage(glob.LastErrorText);
finally
glob.Free;
end;
Example 2: replacing TaskChain
A TaskChain ran a sequence of operations one after another on a background thread, stopping if a
step failed. In Pascal that is simply a sequence of statements inside Execute — with the
advantage that the steps can be anything at all, not only Chilkat calls.
procedure TUploadJobThread.Execute;
var
sftp: TSFtp;
i: Integer;
begin
sftp := TSFtp.Create;
try
sftp.ConnectTimeoutMs := 10000;
sftp.IdleTimeoutMs := 20000;
// Step 1
FStatus := 'Connecting...';
Queue(ReportStatus);
if not sftp.Connect('sftp.example.com', 22) then
begin FErrorText := sftp.LastErrorText; Exit; end;
// Step 2
if not sftp.AuthenticatePw('mylogin', 'mypassword') then
begin FErrorText := sftp.LastErrorText; Exit; end;
// Step 3
if not sftp.InitializeSftp then
begin FErrorText := sftp.LastErrorText; Exit; end;
// Step 4 -- a loop, which no TaskChain could express
for i := 0 to FFiles.Count - 1 do
begin
if Terminated then Exit;
FStatus := Format('Uploading %d of %d...', [i + 1, FFiles.Count]);
Queue(ReportStatus);
if not sftp.UploadFileByName('/upload/' + ExtractFileName(FFiles[i]),
FFiles[i]) then
begin
FErrorText := sftp.LastErrorText;
Exit;
end;
// ...and here you can do anything else at all: write a database
// row, move the local file to an archive folder, log the result.
RenameFile(FFiles[i], FArchiveDir + ExtractFileName(FFiles[i]));
end;
sftp.Disconnect;
FSuccess := True;
finally
sftp.Free;
Synchronize(ReportDone); // runs whether we succeeded or bailed out
end;
end;
Reading top to bottom tells you exactly what happens and in what order. There is no chain object to build, no
callback to register, and the failure path is an ordinary Exit.
Example 3: many operations in parallel
This replaces RunOnThreadPool and Global.MaxThreads. A fixed number of worker
threads pull URLs from a shared, lock-protected queue; the main thread waits for all of them and totals the
results. This is a complete, compilable console program.
program ParallelDownloads;
{$mode delphi}{$H+}
uses
{$IFDEF UNIX}cthreads,{$ENDIF} // REQUIRED on Linux and macOS
Classes, SysUtils, SyncObjs,
Chilkat.Global, Chilkat.Http;
type
{ A trivial thread-safe work queue. }
TWorkQueue = class
private
FLock: TCriticalSection;
FItems: TStringList;
FNext: Integer;
public
constructor Create(AItems: TStrings);
destructor Destroy; override;
function TryGetNext(out AUrl: string): Boolean;
end;
TDownloadWorker = class(TThread)
private
FQueue: TWorkQueue;
FOkCount: Integer;
protected
procedure Execute; override;
public
constructor Create(AQueue: TWorkQueue);
property OkCount: Integer read FOkCount;
end;
constructor TWorkQueue.Create(AItems: TStrings);
begin
inherited Create;
FLock := TCriticalSection.Create;
FItems := TStringList.Create;
FItems.Assign(AItems);
FNext := 0;
end;
destructor TWorkQueue.Destroy;
begin
FItems.Free;
FLock.Free;
inherited Destroy;
end;
function TWorkQueue.TryGetNext(out AUrl: string): Boolean;
begin
FLock.Acquire;
try
Result := FNext < FItems.Count;
if Result then
begin
AUrl := FItems[FNext];
Inc(FNext);
end;
finally
FLock.Release;
end;
end;
constructor TDownloadWorker.Create(AQueue: TWorkQueue);
begin
FQueue := AQueue;
FOkCount := 0;
FreeOnTerminate := False; // the main thread calls WaitFor, then frees
inherited Create(False);
end;
procedure TDownloadWorker.Execute;
var
http: THttp;
url: string;
begin
// One THttp per worker. Never share a Chilkat object across threads.
http := THttp.Create;
try
while (not Terminated) and FQueue.TryGetNext(url) do
begin
if http.Download(url, ExtractFileName(url)) then
Inc(FOkCount)
else
WriteLn('FAILED ', url, ' : ', http.LastErrorText);
end;
finally
http.Free;
end;
end;
const
NUM_WORKERS = 4;
var
glob: TGlobal;
urls: TStringList;
queue: TWorkQueue;
workers: array[0..NUM_WORKERS - 1] of TDownloadWorker;
i, urlCount, okTotal: Integer;
begin
// 1) Unlock once, on the main thread, before starting any worker.
glob := TGlobal.Create;
try
if not glob.UnlockBundle('Anything for 30-day trial') then
begin
WriteLn(glob.LastErrorText);
Halt(1);
end;
finally
glob.Free;
end;
urls := TStringList.Create;
try
urls.Add('https://www.example.com/file1.zip');
urls.Add('https://www.example.com/file2.zip');
urls.Add('https://www.example.com/file3.zip');
urls.Add('https://www.example.com/file4.zip');
urls.Add('https://www.example.com/file5.zip');
urlCount := urls.Count;
queue := TWorkQueue.Create(urls);
try
// 2) Start the workers. Four downloads run concurrently.
for i := 0 to NUM_WORKERS - 1 do
workers[i] := TDownloadWorker.Create(queue);
// 3) Wait for all of them -- the equivalent of task.Wait().
okTotal := 0;
for i := 0 to NUM_WORKERS - 1 do
begin
workers[i].WaitFor;
Inc(okTotal, workers[i].OkCount);
workers[i].Free;
end;
finally
queue.Free;
end;
finally
urls.Free;
end;
WriteLn(Format('%d of %d downloads succeeded.', [okTotal, urlCount]));
end.
cthreads. It has to be the
first unit in the program's uses clause, as shown above. Without it the program compiles but
threading does not work correctly at run time. Lazarus GUI projects created from the LCL templates already
include this in the .lpr; console projects usually do not.
Synchronize. Synchronize and Queue hand work to
the main thread's message loop. An LCL application has one. A console program does not, so a queued method never
runs and Synchronize blocks forever. In a console program, either avoid them entirely (as Example 3
does — the workers just write to the console and return counts) or call CheckSynchronize
periodically from the main thread while it waits.