Assigning result to a function from asynchronous code
One of the more common problems that comes up in multi-threading, especially when refactoring existing code, is assigning a result obtained from asynchronous code. In other words, how do you write a function that will return some value, and calculate that value in a background thread?
Basically, converting the following code:
function GetData(...): string; var Data: string; begin Data := LongTask(...); // assign result of a long-running task to a function Result := Data; end;
To this one:
function GetData(...): string; var Data: string; begin TThread.CreateAnonymousThread( procedure begin Data := LongTask(...); end).Start; // assign result of a long-running task to a function Result := Data; end;
Now, the above code will compile, but it will not achieve the desired functionality. Even if
we ignore its thread safety issues (the background thread writes to the
Data variable and
that can interfere with assigning it to the function result), the function
will return long before the anonymous thread and the
LongTask have finished the work
and have had a chance to assign something to
The actual effect of running such a
GetData function will simply be to return an empty string.
If we try to move the result assignment into a thread, we will not be able to compile such code, and we will get an error:
E2555 Cannot capture symbol 'Result'
One way of retrieving the result from a thread is waiting for the thread to finish its work, and then using whatever data it has provided.
function GetData: string; var Data: string; Thread: TThread; begin Thread := TThread.CreateAnonymousThread( procedure begin Data := LongTask; end); // if we want to wait for a thread // we need to manually manage its memory Thread.FreeOnTerminate := False; Thread.Start; Thread.WaitFor; Thread.Free; Result := Data; end;
Now we have successfully retrieved the data from the thread. But what we have accomplished by waiting for the thread is just a more complicated variant of the original single-threaded function.
Because the main reason for using background threads for running long tasks is keeping the application responsive, our goal is not achieved. Yes, we will run the long task in a background thread, but then waiting for the background thread will still block the main thread and make our application unresponsive.
Of course, you could make a more elaborate waiting mechanism which will also
include pumping the message queue with
Application.ProcessMessages, but this kind of
code just opens another can of worms.
The proper solution is to refactor a function into a procedure, and use a callback that will receive the data once it is retrieved. You need to move from running the code synchronously, where calling a function will return a result, into event-driven code that will run some code when some event happens—in this case, when the background thread retrieves the data we need.
This is similar to the existing Delphi application architecture: when the user clicks a
button and if the button has an associated
OnClick event handler, the code in that
event handler will run.
The same principle applies on threads: When a thread finishes, it can call an associated method (callback or completion handler) and pass data to that callback procedure, or it can use some kind of event (message) dispatching system to send a event with the data attached.
TThread class already has a build-in callback mechanism in the form of
notification event, but this is a plain
TNotifyEvent method, where the sender will be
the associated thread, and to pass data to that event, you either need to use a custom
thread class that will store data in a property, or you need to involve other
variables in a broader scope.
There is another way to achieve similar functionality that can work on any kind of thread, including anonymous ones: Using a different callback procedure that defines parameters for all the data we need to return from a thread:
procedure GetData(...; OnDataRetrieved: TProc<string>); begin TThread.CreateAnonymousThread( procedure var Data: string; begin Data := LongTask(...); TThread.Synchronize(nil, procedure begin OnDataRetrieved(Data); end); end).Start; end;
And the usage of
GetData changes from:
procedure TForm1.ButtonClick(Sender: TObject); var Data: string; begin Data := GetData(...); Memo1.Lines.Add(Data); // do anything else with data here end;
to the following code, using an anonymous method as a callback:
procedure TForm1.ButtonClick(Sender: TObject); begin GetData(..., procedure(Data: string) begin Memo1.Lines.Add(Data); // do anything else with data here end); GetData(ProcessData); end;
or the following code, using an additional regular method as a callback:
procedure TForm1.ButtonClick(Sender: TObject); begin GetData(..., ProcessData); end; procedure TForm1.ProcessData(Data: string); begin Memo1.Lines.Add(Data); // do anything else with data here end;
Note: Synchronizing callback
OnDataRetrieved with the main thread is needed because we are accessing UI in the callback and we can only do that safely from the context of the main thread. Synchronizing callback with the main thread also matches the behavior of
OnTerminate event handler which also runs synchronized with the main thread.