4

I would say the following two code snippets I have are equivalent, but they aren't.

The following is working correctly:

var entry3 = Task.Run(async () => await entry2.GetMemberGroupsAsync(false)).WaitForResult().FirstOrDefault();

The following code, where I just moved the Task.Run.WaitForResult chain into an extension method, isn't working, but produces a deadlock:

var entry3 = entry2.GetMemberGroupsAsync(false).RunSynchronouslyAndReturnResult().FirstOrDefault();

public static T RunSynchronouslyAndReturnResult<T>(this Task<T> task)
{
    return Task.Run(async () => await task).WaitForResult();
}

Why aren't these two code snippets equivalent?

For completeness's sake, the GetMemberGroupsAsync method is provided by Microsoft Azure Graph API, and the function WaitForResult is defined below. As far as I can see, it doesn't do anything different depending on the caller name or sth. like that:

public static TResult WaitForResult<TResult>(this Task<TResult> task,
                                             bool continueOnCapturedContext = false)
{
    if (task == null)
    {
        throw new ArgumentNullException("task");
    }

    try
    {
        return PreventForDeadLocks(task, continueOnCapturedContext).Result;
    }
    catch (AggregateException ex)
    {
        if (ex.InnerExceptions.Count == 1)
        {
            throw ex.InnerExceptions[0];
        }

        throw;
    }
}

public static async Task<TResult> PreventForDeadLocks<TResult>(this Task<TResult> task,
                                                               bool continueOnCapturedContext = false)
{
    return await task.ConfigureAwait(continueOnCapturedContext: continueOnCapturedContext);
}
Alexander
  • 19,906
  • 19
  • 75
  • 162
  • 1
    Even worse, `PreventForDeadlocks` won't prevent deadlocks. **There is no sync-over-async pattern that works in all situations!** – Stephen Cleary Jun 07 '16 at 12:01
  • Why not just do `var entry3 = (await entry2.GetMemberGroupsAsync(false)).FirstOrDefault();`? Is it because that would require you to make the method that this code is inside `async`? Typically you let the `async`-`await` pattern bubble up to your events and then make those `async void` (fire and forget). – juharr Jun 07 '16 at 12:32
  • @juharr just that I don't have events - the code is inside a WebAPI endpoint. Can I make that WebAPI endpoint async? – Alexander Jun 07 '16 at 13:04

2 Answers2

4

The difference here is in which synchronization context your task started. Here:

var entry3 = Task.Run(async () => await entry2.GetMemberGroupsAsync(false)).WaitForResult().FirstOrDefault();

you start your async task (I mean await entry2.GetMemberGroupsAsync(false)) inside Task.Run call, so UI synchronization context is not captured. But here:

var entry3 = entry2.GetMemberGroupsAsync(false).RunSynchronouslyAndReturnResult().FirstOrDefault();

You implicitly start your task (entry2.GetMemberGroupsAsync(false) returns Task) on UI context, so UI synchronization context is captured, and you have your deadlock.

Evk
  • 98,527
  • 8
  • 141
  • 191
3

In the first case, GetMemberGroupsAsync is called on a different thread than WaitForResult.

In the second case, it is called on the same thread as WaitForResult. You're just awaiting on a different thread.

Eren Ersönmez
  • 38,383
  • 7
  • 71
  • 92
  • Why is that; is the Extension method executed on a different thread? – Alexander Jun 07 '16 at 12:12
  • In the first case, GetMemberGroupsAsync is called within the Task.Run, so it is run on a different thread. In the second case, GetMemberGroupsAsync is called first (so the task has already started), and then the resulting task is passed to Task.Run. So in the second case, GetMemberGroupsAsync is _not_ called on a different thread. Makes sense? – Eren Ersönmez Jun 07 '16 at 13:17