0

I was wondering, would there be any gotchas from using an extension method for Task<T> like so:

public static T Await<T>(this Task<T> task)
{
    var result = default(T);

    Task.Run(async () => result = await task).Wait();

    return result;
}

It seems like a decent time saver for those instances where you would like to get the result from a Task but you're in a method that is not marked with async.

mfluehr
  • 2,832
  • 2
  • 23
  • 31
Clayton Roth
  • 95
  • 2
  • 6
  • 4
    Or just task.Result - It does the same – Sir Rufo Oct 18 '16 at 23:38
  • 1
    http://stackoverflow.com/a/39371523/477420 contains information you need to answer your question. – Alexei Levenkov Oct 19 '16 at 00:13
  • 2
    @SirRufo `tast.Result` will deadlock almost all the time unlike code provided in the post that completely broken only for tasks that actually do something useful (like update UI elements when result arrives) and *only* freezes current thread. – Alexei Levenkov Oct 19 '16 at 00:15
  • @AlexeiLevenkov I think Sir Rufo meant `result = Task.Run(async () => await task).Result;` – Scott Chamberlain Oct 19 '16 at 01:26
  • I also agree with @AlexeiLevenkov link to the duplicate at http://stackoverflow.com/questions/5095183/how-would-i-run-an-async-taskt-method-synchronously/39371523 but I can close C# questions with a single vote and don't feel this is quite close enough to do a single vote close (the duplicate is "how do I do it", and this is "What is wrong with doing it this way"). – Scott Chamberlain Oct 19 '16 at 01:41

1 Answers1

8

You code will not work like you want because you are passing in a "Hot Task" to the function.

I assume the reason you are doing this is to prevent the deadlock of just calling task.Result. The reason the deadlock happens is you are blocking the UI thread and the task's captured sync context uses the UI thread for it's postbacks. The problem is the context is captured when the task starts not when you await it.

So if you did on your UI thread

Task<Foo> task = SomeMethodAsync();
Foo result = task.Await();

you are still going to deadlock because the SynchronizationContext that SomeMethodAsync() captured is the UI context, and any internal await inside SomeMethodAsync() that does not use .ConfiguerAwait(false) will try to use the UI thread which will be blocked by your .Wait() call in Await().

The only way to reliably get it to work is if the method took in a Func<Task<T>> instead of just Task<T>, you could then start the task in the background thread to ensure the sync context is not set.

public static T BlockWithoutLockup<T>(Func<Task<T>> task)
{
    T result;

    if(SynchronizationContext.Current != null)
    {
        //We use ".GetAwaiter().GetResult()" instead of .Result to get the exception handling 
        // like we would if we had called `await` or the function directly.
        result = Task.Run(task).GetAwaiter().GetResult();
    }
    else
    {
        //If we are on the default sync context already just run the code, no need to
        // spin up another thread.
        result = task().GetAwaiter().GetResult();
    }
    return result;
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Great explanation, but I would not recommend using Task.Run for async. – Erik Philips Oct 19 '16 at 01:41
  • 3
    @ErikPhilips I would recommend not running asynchronous code synchronously at all. If it was me I would either use a different method that was not async or re-write my function to be async long before I ever try this. – Scott Chamberlain Oct 19 '16 at 01:46
  • I've needed to work with NServiceBus 5.x where you must implement a synchronous method call, and found it necessary to call async APIs. I've found that Task.Run(async () => result = await SomeMethod()).Wait() was the preferred solution based on Stephen Cleary's comments (e.g. http://stackoverflow.com/questions/18013523/when-correctly-use-task-run-and-when-just-async-await) so I'm suprised that this code would inherently fail. Out of curiosity I created a simple console, and WPF application that used the Await extension method and I haven't been able to produce a deadlock – Kevin Oct 19 '16 at 20:04
  • @Kevin Create a WPF app, subscribe to the Loaded event for the window and use [this code for the body](https://gist.github.com/leftler/01bee2de06fd907417a966ea90d47d65), it will cause a deadlock. The problem is he is not doing `Task.Run(async () => result = await SomeMethod()).Wait()` he is doing `Task.Run(async () => result = await task).Wait();`, you ***start*** the task inside the `Task.Run` but he passes in a "Hot Task" in to the function. – Scott Chamberlain Oct 19 '16 at 21:18
  • @ScottChamberlain, I definitely see the argument made for where the task originates, but I'm calling his extension method thus I am passing the awaitable task into it. I had it wired to a button click, but I'm not getting a deadlock on a window OnLoaded either – Kevin Oct 19 '16 at 23:11
  • @Keven if you are not getting a deadlock then you are not using the OP's original `Await(this Task task)`, you likely have `Await(this Func> task)` or else you did not have a `await` in the function that was passed in that needed to call back to the UI thread (either you had no `await` in it or you used `ConfigureAwait(false)` inside the task). – Scott Chamberlain Oct 19 '16 at 23:28
  • @ScottChamberlain, I realized the distinction, if I use the Await extension method on a task which internally awaits that does not ConfigureAwait(false), a deadlock will occur. In my case I was calling HttpClient.GetAsync which most likely does so it "worked" without a deadlock. Hopefully this clarifies the multiple scenarios – Kevin Oct 20 '16 at 05:41