This project has moved and is read-only. For the latest updates, please go here.

Synchronous.TaskExtensions.Wait* Questions

Apr 30, 2014 at 12:23 PM
Stephen - thanks for your work! I find it very useful

I'm programming against an API that requires me to provide an Action and a Func<bool>. I wish to satisfy these with async code I have written in my projects. It looks like your Synchronous.TaskExtensions offers a good solution.

First off, I've read a good deal of your writings: don't try to bridge async/synchronous code, "async all the way", etc. So am I even going about this the right way? How should I satisfy these 3rd party Actions and Funcs? Assuming this is an acceptable exception to the rules...

In several places I'm curious to understand why you use Task.GetAwaiter().GetResult() instead of just Task.Wait() or Task.Result()

In fact, why not just delegate to your overloads that support cancellation by passing CancellationToken.None?

Thanks,

Nate
May 1, 2014 at 8:31 PM
Edited May 1, 2014 at 8:32 PM
I am hesitant that this would work; it really depends on the API and the context in which your delegates are executed. The best solution is to update the API to support Func<Task> (the asynchronous equivalent of Action) and Func<Task<bool>> (the asynchronous equivalent of Func<bool>).

That said, here's the reasoning behind my code:

Task.GetAwaiter().GetResult() works essentially the same as Task.Wait / Task.Result but with one important difference in the error scenario: GetAwaiter().GetResult() will rethrow the task's first exception, preserving its stack trace (this is the same behavior as await); while Task.Wait and Task.Result will wrap all the task's exceptions in a single AggregateException and throw that. (Note that in async code, Tasks normally can only have a single exception; Task.WhenAll is the exception to this rule, and even in that case the extra exceptions usually don't matter).

Earlier versions of the code actually wrapped a Task.Wait / Task.Result call in a try/catch, extracted the first exception, and re-threw it (preserving the stack). GetAwaiter().GetResult() logically does the same thing but in a much more efficient manner.

The reason these overloads don't delegate to the CancellationToken overloads is because once you have a CancellationToken, synchronously waiting on a task is much less efficient. Task.AsyncWaitHandle incurs a good performance penalty described by Stephen Toub on his blog.

-Steve
May 1, 2014 at 11:40 PM
Thanks for the insight

For completeness (sorry I should have included this originally) - MVVM app, I'm using Prism's DelegateCommand (http://goo.gl/jn5djG) implementation of ICommand (http://goo.gl/t1s5MC), which requires that I provide synchronous delegates for Execute and CanExecute methods. I need Execute to call async code that I've written (TcpClient stuff)

Nate
May 2, 2014 at 2:49 AM
You may find a recent MSDN article of mine helpful.

In particular, async ICommand implementations are an exception to the general rule of avoiding async void. I do recommend creating an AsyncCommand for testability. The AsyncCommand then gives you a Func<Task>-based API, while converting it to an async void method underneath.

-Steve
May 2, 2014 at 4:06 PM
Thanks for that link (unfortunately I missed it in my original searching!) - you've put together a very useful way to expose the Task "baggage" in a data-bindable way

Unfortunately CanExecute is basically ignored in the article (no implementation is provided in the ViewModel, only the base implementation which depends on state of the wrapped Task). What if, for instance, enabling of a command depended on privileges stored in a database? I suppose the CanExecute implementation could depend on a new async property backed by your NotifyTaskCompletion class? [sorry, thinking out loud]

Back to the original discussion, given you comments about cancellation tokens, then could/should your code check for CancellationToken.None and delegate back to the non-CancellationToken overload?

Finally, what are your thoughts on Task.RunSynchronously as related to this discussion? Are your TaskExtensions.Wait* methods still a better way to go?

I'm thinking I'll use your MSDN techniques for my current MVVM/ICommand implementation, but I'm still interested in having a solution to the more general synchronous-to-asyc bridging issue. There are other 3rd party APIs out there - that cannot be changed by me - that require a Func<...,T> for which I might want to provide an async implementation. I'd like to have a best-practice pattern in my back pocket for when those cases arise.

Thanks again,

Nate
May 2, 2014 at 6:25 PM
CanExecute has to be synchronous. In every UI I've ever written, by the time I'm displaying the UI, user permissions etc. have already been loaded. (And I tend to hide controls in that scenario rather than disable, anyway).

The CancellationToken overloads can delegate to the non-CT overloads if !CancellationToken.CanBeCanceled. I'll take that into consideration for the next release, though it is an optimization for a very rare scenario. I moved those TaskExtensions into the Synchronous namespace just before 1.0.0 to prevent them from regularly showing up in IntelliSense. They shouldn't be used often, and they very nearly escaped being cut completely from the project.

Task.RunSynchronously has a completely different purpose: it executes a Delegate Task. Delegate Tasks are rare in async/await code (most tasks are Promise Tasks), and RunSynchronously will throw an exception if you call it on a Promise Task. Also, most Delegate Tasks are created running, and RunSynchronously will also throw in that scenario. The Synchronous.Wait* methods do not attempt to execute the task if it's not already running; they assume the task is already running and just wait for it to complete (this works for both Delegate and Promise Tasks).

There is no general solution for the sync/async bridging issue, and certainly no best practices. The best articles on the subject are from Stephen Toub on sync over async and async over sync. He describes every solution I know of for going both ways, but every single solution has some drawbacks - usually pretty severe ones.

-Steve