Tuesday, July 31, 2012

Async/Await in Delphi

Let’s assume you’ve inherited this pretty useless code.
procedure TForm125.Button1Click(Sender: TObject);
var
  button: TButton;
begin
  button := Sender as TButton;
  button.Caption := 'Working ...';
  button.Enabled := false;
  Sleep(5000);
  button.Enabled := true;
  button.Caption := 'Done!';
end;

Now, your boss says, you have to make it parallel so the user can start three copies of it. (You also have to add two new buttons to the form to start those instances but that’s easy to do.)

There are many ways to solve this problem, some more and some less complicated; I’d like to point out a simplest possible solution. But first, let’s take a detour into .NET waters …
.NET 4.5 introduced a heavy magical concept of ‘async’ and ‘await’ (learn more). In short, it allows you to write a code like this:

procedure TForm125.Button1Click(Sender: TObject); async;
var
  button: TButton;
begin
  button := Sender as TButton;
  button.Caption := 'Working ...';
  button.Enabled := false;
  await CreateTask(
    procedure begin
      Sleep(5000);
    end);
  button.Enabled := true;
  button.Caption := 'Done!';
end;

[Please note that this is not a supported syntax; that’s just an example of how the .NET syntax could look if Delphi would have supported it.]

The trick here is that ‘await’ doesn’t really wait. It relinquishes control back to the main loop which continues to process events etc. In other words – the rest of the program is running as usual. It may also call another asynchronous function and ‘await’ on it. Only when an asynchronous function returns (any of them, if there are more than one running), the control is returned to the point of the appropriate ‘await’ call and the code continues with the next line.

This is something that is possible to do only with the extensive support of the compiler and there’s absolutely no way to write an async/await clone in Delphi. But … there’s a simple trick which allows you to write the code in almost the same way. It uses OmniThreadLibrary’s Async construct and the magic of anonymous methods.

procedure TForm125.Button1Click(Sender: TObject);
var
  button: TButton;
begin
  button := Sender as TButton;
  button.Caption := 'Working ...';
  button.Enabled := false;
  Parallel.Async(
    procedure begin
      Sleep(5000);
    end,
    Parallel.TaskConfig.OnTerminate(
      procedure begin
        button.Enabled := true;
        button.Caption := 'Done!';
      end));
end;

Async executes its parameter (the delegate containing the Sleep call) in a background thread. When this background task is completed, it executes the second parameter (the OnTerminate delegate) in the main thread. While the background task is working, the main thread spins in its own message loop and runs the user interface – just as it would in the .NET case.

With some syntactical sugar, you can fake a very convincing .NET-like behavior.

type
  IAwait = interface
    procedure Await(proc: TProc);
  end;

  TAwait = class(TInterfacedObject, IAwait)
  strict private
    FAsync: TProc;
  public
    constructor Create(async: TProc);
    procedure Await(proc: TProc);
  end;

function Async(proc: TProc): IAwait;
begin
  Result := TAwait.Create(proc);
end;
{ TAwait }

constructor TAwait.Create(async: TProc);
begin
  inherited Create;
  FAsync := async;
end;

procedure TAwait.Await(proc: TProc);
begin
  Parallel.Async(FAsync, Parallel.TaskConfig.OnTerminated(
  procedure begin
    proc;
  end));
end;
{ TForm125 }
procedure TForm125.Button1Click(Sender: TObject);
var
  button: TButton;
begin
  button := Sender as TButton;
  button.Caption := 'Working ...';
  button.Enabled := false;
  Async(
    procedure begin
      Sleep(5000);
    end).
  Await(
    procedure begin
      button.Enabled := true;
      button.Caption := 'Done!';
    end);
end;

To test, put three buttons on a form and assign the Button1Click handler to all three. Click and enjoy.

18 comments:

  1. Oooo! Very nice! Async/await was one of the few places where .NET provides a clear advantage over Delphi programming. And now you show it's this easy to emulate the style? Awesome!

    ReplyDelete
  2. The syntax in second code block is not the same as in the last blocks. The second code block only offloads Sleep into separate routine. The last code blocks do the same for ending code of Button1Click.

    It must be something like this to copy logic from second code block:

    begin
    button := Sender as TButton;
    button.Caption := 'Working ...';
    button.Enabled := false;
    Parallel.AsyncAndWait(
    procedure begin
    Sleep(5000);
    end);
    button.Enabled := true;
    button.Caption := 'Done!';
    end;

    ReplyDelete
    Replies
    1. I don't get it, sorry. I checked again and I believe that the syntax is correct.

      Delete
    2. The point of this is that it *isn't* "async and wait", but "async and continue other operations, and then come back to it when the async task is done." C# manages this by having the compiler rewrite the code to something that works more or less like what Gabr did here. Without compiler support, though, the only way to get this to work right is something like the example.

      Delete
    3. Mason, thanks for the explanation.

      Delete
  3. Anonymous20:10

    >> And now you show it's this easy to emulate the style? Awesome!
    Is it emulation ? Are you kidding??

    C# 4.5
    {
    responseFromServer1 = await SendDataAsync(server1, data1);
    responseFromServer2 = await SendDataAsync(server2, responseFromServer1);
    responseFromServer3 = await SendDataAsync(server2, responseFromServer2);
    result = await SendDataAsync(server4, responseFromServer3);
    }
    Delphi

    Async(
    procedure begin
    ResponseFromServer1 := SendData(Server1, Data);
    end).
    Await(
    procedure begin
    Async(procedure begin
    ResponseFromServer2 := SendData(Server2, ResponseFromServer1);
    end).
    Await(
    procedure begin
    Async(procedure begin
    ResponseFromServer3 := SendData(Server3, ResponseFromServer2);
    end).
    Await(begin begin
    Result := := SendData(Server4, ResponseFromServer3);
    end)
    end
    );
    end);

    ReplyDelete
    Replies
    1. I'm not responsible for long-wordness of Delphi's anonymous methods.

      Delete
    2. Anonymous08:22

      Trouble not at lambdas.
      actually in C#4 with good labmda syntax, it's practically impossible to write this code:

      while (await BoolMethodAsync(...))
      {
      int x;
      if (await ConditionAsync()) x = MethodSync(...)
      else x = await MethodAsync(...);
      if (x > 10)
      {
      try
      {
      await Method2Async();
      }
      finally
      {
      Method3Sync();
      }
      }
      }

      Delete
    3. Frankly, I like the explicit use of Delphi "async" blocks and then "after completion" block better than the C# "magic" approach. Well done GABR.

      W

      Delete
  4. Thank you, I am not a .NET fan and was interested how this .NET await feature can work internally; I guess that in can be implemented only using fibers in plain WinAPI. Your trick is good but sure the await feature is much more powerful, it made me think of the concept of coroutines.

    PS: I for the reason unknown your site treat me as anonymous robot, though I register with my OpenID. Very annoying.

    ReplyDelete
    Replies
    1. As far as I know, .NET C# compiler decomposes methods using 'await' into a state machine and then runs this state machine when an event is 'awaited'.

      RE: PS, sorry, but that's Blogger acting up and I can't fix it :(

      Delete
    2. Hi! Carlo from RemObjects just posted blog post that answer exactly your question: how .NET await acts in background, behind the scene)) Here it is: http://blogs.remobjects.com/blogs/ck/2012/08/08/p4690

      Delete
  5. Nice.. What if you get an exception in the ASync/AWait delphi example ?

    Any plans on adding the Async/Await sugar to OmniThread ?

    Thanks
    Andrew

    ReplyDelete
    Replies
    1. Exceptions are handled the same as in the Async, see http://otl.17slon.com/book/doku.php?id=book:highlevel:async

      I don't know yet whether Async/Await will be added to the OtlParallel. Probably yes.

      Delete
    2. Thanks... I'd love to see it as part of OtlParallel..

      As a C#/Delphi coder, its nice to have similar sugar available.

      I'd like it included if possible..

      Delete
    3. Does the Await Sugar need to be extended to handle the Async Exception ?

      procedure TAwait.Await(proc: TProc);
      begin
      Parallel.Async(FAsync, Parallel.TaskConfig.OnTerminated(
      procedure (const task: IOmniTaskControl)
      begin
      proc(task);
      end));
      end;

      Could some extra sugar be added to make exceptions clean also ?

      Can you give a really simple example of an exception and this sugar ?

      Thanks.
      Again, awesome job.

      Delete
  6. I wonder if there can be "Await with guards"

    For example in one forum there is a person, who need to make a conenction through some private library, which is blocking and which does not have timeout concept. Initially he asked how to set TTimer in Delphi to simulate timeout. Then he was (and still is) very reluctant to exit the procedure and - MG - wants to look ProcessMessages instead. He definitely doesn't know how to split his task into events...

    So it perhaps be cool to see constructs like

    Async( function: TValue begin ... end ).OnTimeout( 10000 ms, procedure ... end).AwaitWhen(const result: TValue, procedure .... end).AwaitWhen(....,....).Await(procedure ... end);

    Like case x,y,z,else end;

    This however leaves quite a question how to terminate spawned task on timeout (and whether it should be terminated or just its exit value ignored from now on)

    ReplyDelete