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.
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!
ReplyDeleteThe 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.
ReplyDeleteIt 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;
I don't get it, sorry. I checked again and I believe that the syntax is correct.
DeleteThe 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.
DeleteMason, thanks for the explanation.
Delete>> And now you show it's this easy to emulate the style? Awesome!
ReplyDeleteIs 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);
I'm not responsible for long-wordness of Delphi's anonymous methods.
DeleteTrouble not at lambdas.
Deleteactually 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();
}
}
}
Frankly, I like the explicit use of Delphi "async" blocks and then "after completion" block better than the C# "magic" approach. Well done GABR.
DeleteW
Very cool.
ReplyDeleteThank 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.
ReplyDeletePS: I for the reason unknown your site treat me as anonymous robot, though I register with my OpenID. Very annoying.
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'.
DeleteRE: PS, sorry, but that's Blogger acting up and I can't fix it :(
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
DeleteNice.. What if you get an exception in the ASync/AWait delphi example ?
ReplyDeleteAny plans on adding the Async/Await sugar to OmniThread ?
Thanks
Andrew
Exceptions are handled the same as in the Async, see http://otl.17slon.com/book/doku.php?id=book:highlevel:async
DeleteI don't know yet whether Async/Await will be added to the OtlParallel. Probably yes.
Thanks... I'd love to see it as part of OtlParallel..
DeleteAs a C#/Delphi coder, its nice to have similar sugar available.
I'd like it included if possible..
Does the Await Sugar need to be extended to handle the Async Exception ?
Deleteprocedure 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.
I wonder if there can be "Await with guards"
ReplyDeleteFor 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)
Your dead wrong:
ReplyDelete"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."
The .Net await stuff is just fiber switching - no compiler magic. Delphi supports fibers.
The most beautiful result of C#'s async-await is that the keyword await can be used inside a loop. The compiler will rewrite the code to a state machine, which is equivalent to putting a recursive function in the Await call. Manually writing an recursive function and putting it in the Await call is painful, this normally require us to do a lot of copy.
ReplyDeleteHello,
ReplyDeletei've just found out that async/await construction in OTL works just from the main thread. I don't know if it is mentioned to behave like this so I have decided to report this issue just to take a look on it. Maybe the cause of this behavior may cause some other unexpected things in OTL.
Regards, Jaroslav
Async/Await should work fine in a thread provided that this thread processes messages. (For example, if it is a low-level OTL task created with CreateTask, you should apply .MsgWait modifier to it.=
DeleteHow to handle exceptions in async/await?
ReplyDeleteEither handle exceptions yourself (add try..except inside the async and await blocks) or use Parallel.Async (http://www.omnithreadlibrary.com/book/chap06.html#highlevel-async).
Delete