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 Data.

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.

The TThread class already has a build-in callback mechanism in the form of OnTerminate 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.

Comments

  1. You should have pointed out that using TThread.Synchronize is very important here because the callback procedure is called in the context of the thread but accesses the VCL, so it must switch to the main thread in order to do that safely.

    ReplyDelete
    Replies
    1. Thanks! I added a small note for those that might wonder why is synchronize used there.

      Delete
    2. This approch called continuation passing style and lead to callback hell (see JavaScript before async/await came)
      Task/feature-based style is way more readable, IMHO

      consider we have 3 long operations and wanna combine them

      Sync style
      function LongOp1(Input: Interger): string;
      function LongOp2(Input: string): TDateTime;
      function LongOp3(Input: TDateTime): TObject;

      var Op1Result := LongOp1(0);
      var Op2Result := LongOp2(Op1Result);
      var Op3Result := LongOp3(Op2Result);
      // do something with Op3Result

      Continuation passing style:
      procedure LongOp1(Input: Interger; const AContinuation: TProc);
      procedure LongOp2(Input: string; const AContinuation: TProc);
      procedure LongOp3(Input: TDateTime; const AContinuation: TProc);


      LongOp1(0,
      procedure (Op1Result: string)
      begin
      LongOp2(Op1Result,
      procedure(Op2Result: TDateTime)
      begin
      LongOp3(Op2Result,
      procedure (Op3Result: TObject)
      begin
      // You see, every LongOpX call increases nesting of your code
      // do something with Op3Result
      end);
      end);
      end)

      Task/feature style
      TFuture = record
      type TFutureFunc = reference to function (AResult: TInput): TTask;
      function ContinueWith(AContinuation: TFutureFunc): TFuture; overload;
      procedure ContinueWith(AContinuation: TProc); overload;
      end;

      function LongOp1(Input: Interger): TFuture;
      function LongOp2(Input: string): TFuture;
      function LongOp3(Input: TDateTime): TFuture;

      var Op1ResultFuture := LongOp1(0);
      var Op2ResultFuture := Op1ResultFuture.ContinueWith(LongOp2);
      var Op3ResultFuture := Op1ResultFuture.ContinueWith(LongOp3);
      Op3ResultFuture.ContinueWith(
      procedure (Op3Result: TObject)
      begin
      // do something with Op3Result
      end);

      Delete
    3. Callback hell can be an issue if there are several levels of callbacks. Using some kind of Future/Continuation framework can solve that part.

      However, the simplest and the most effective solution would be running those multiple long tasks that need to be done one after another in a single background thread/task which has single callback if necessary.

      Delete
  2. That's a nice way to deal with the problem, yes.
    However, if you have a desktop application and a VCL Form is involved in this process, I still prefer to use messages. Pass the handle of the window to the threaded GetData() method and it can call SendMessage/PostMessage when done. It's easier to implement a more complex logic that deals with different (unexpected) conditions like errors. The biggest bonus is that you get rid of Synchronize() completely.

    ReplyDelete
    Replies
    1. This was just example of how to use callbacks feature.

      Messaging (windows default messaging or using some other messaging framework) is another way of handling completion, but it can be more complicated to setup in some situations instead of having a simple callback method.

      Additional problem with Windows messaging approach and using VCL controls is that its handle can become invalid in the meantime as handles that belong to windowed controls can be recreated during control lifetime. You could allocate separate handle just for messaging purposes, though.

      Delete

Post a Comment

Popular posts from this blog

Catch Me If You Can - Part II

Delphi 12.1 & New Quality Portal Released

Coming in Delphi 12: Disabled Floating-Point Exceptions