TPL: Composing tasks
What happens when you want to compose two distinct async operations into a single Task?
For example, let us imagine that we want to have a method that looks like this:
public void ConnectToServer() { var connection = ServerConnection.CreateServerConnection(); // tcp connect connection.HandShakeWithServer(); // application level handshake }
Now, we want to make this method async, using TPL. We can do this by changing the methods to return Task, so the API we have now is:
Task<ServerConnection> CreateServerConnectionAsync(); Task HandShakeWithServerAsync(); // instance method on ServerConnection
And we can now write the code like this:
public Task ConnectToServerAsync() { return ServerConnection.CreateServerConnectionAsymc() .ContinueWith(task => task.Result.HandShakeWithServerAsync()); }
There is just one problem with this approach, the task that we are returning is the first task, because the second task cannot be called as a chained ContinueWith.
We are actually returning a Task<Task>, so we can use task.Result.Result to wait for the final operation, but that seems like a very awkward API.
The challenge is figuring a way to compose those two operations in a way that expose only a single task.
Comments
KAE,
Damn it!
I was sure that I had such an elegant solution, and then you come up with this beauty.
Thanks!
However there is a subtle problem. If CreateServerConnection fails you will end up with an unobserved exception which will cause the entire app domain to crash if you don't catch unobserved exception in your app domain.
I use the following task extensions for composing tasks and observe antecedents:
<tresult ObservedContinueWith <tresult(this Task task, Func <task,> continuationFunction, TaskContinuationOptions continuationOptions)
Jesus,
What are you talking about?
Exceptions inside tasks will not kill the app domain
Yes it will. Try this code and see how the console application crashes:
Jesus,
Thanks.
The actual exception is very informative:
A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread.
Which makes total sense.
I forgot to include Console.ReadLine after Console.WriteLine. Anyway, it crashes
If you'd go the Rx path, composition is trivial:
Instead of a Task, you can return an observable of unit (sort of void), and have more native Rx signature:
<unit CreateServerConnectionAsync()
If your whole API will return observables, you could also skip the .ToObservable() task conversions.
I think Omer has the best solution. I have been underwhelmed by the implementation of Task. Cold tasks are basically useless, they forgot to make an ITask... And now Jesus' exception handling issue.
RX doesn't have any of these problems, including the need to unwrap. It is just better thought out.
I hope they add some sort of iterative await for RX before they release 5.0.
@jesus
Note that you can prevent the application crash by subscribing to TaskScheduler.UnobservedTaskException:
Use a TaskCompletionSource and set up your method to return you tc.Task and then wrap your async methods neatly to call tc.TrySetException | TrySetResult when they complete..
Tada...
I "Subscribe" ;-) to the Rx approach.
We use the approach outlined by Omar to load data from mutliple domains services in Ria and works very well.
I'm definitely missing something, but why don't you just put CreateServerConnection and HandShakeWithServer inside one task?
Comment preview