user2712875
user2712875

Reputation: 31

Why doesn't Task<TResult>.Result work in this case?

It is working fine with the following code:

private void button1_Click(object sender, EventArgs e)
{
    Task<int> t = test(5);
    Console.WriteLine(t.Result);
}

private Task<int> test(int n)
{
    return Task.Run(() => 
    {
        return n;
    });
}

But if I wrap the test method with an async method, it doesn't work:

private Task<int> test(int n)
{
    return Task.Run(() =>
    {
        return n;
    });
}

public async Task<int> wrap()
{
    return await test(5);
}

private void button1_Click(object sender, EventArgs e)
{
    Task<int> t = wrap();
    Console.WriteLine(t.Result);
}

The form loses response. If I use await, it works as expected.

Update1: These two answers are both correct, but I can only mark one as answer. Based on the understanding of this question, I did further test. I used ConfigureAwait in wrap method to leave the continuation to run in thread other than UI:

public async Task<int> wrap()
{
    return await test(5).ConfigureAwait(false);
}

It works fine. And then I tested this:

public async Task<int> wrap()
{
    int i = await test(5).ConfigureAwait(false);
    int j = i + await test(3);
    return j;
}

It's working on the first time when I clicked the button, but deadlock again on second click. If I added ConfigureAwait(false) after test(3), like this:

public async Task<int> wrap()
{
    int i = await test(5).ConfigureAwait(false);
    int j = i + await test(3).ConfigureAwait(false);
    return j;
}

it's working again, but this doesn't make sense to me. Because of the first ConfigureAwait(false), all the following synchronization part in wrap() should be run on non-UI thread. I don't understand why the second ConfigureAwait(false) is necessary.

Update2:

private Task<int> test(int n)
{
    return Task.Run(() =>
    {
        Console.WriteLine("test(" + n + "): " + System.Threading.Thread.CurrentThread.ManagedThreadId);
        return n;
    });
}

public async Task<int> wrap()
{
    Console.WriteLine("1.wrap(): " + System.Threading.Thread.CurrentThread.ManagedThreadId);
    int i = await test(5).ConfigureAwait(false);
    Console.WriteLine("2.wrap(): " + System.Threading.Thread.CurrentThread.ManagedThreadId);
    int j = i + await test(3);
    Console.WriteLine("3.wrap(): " + System.Threading.Thread.CurrentThread.ManagedThreadId);
    return j;
}

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Console.WriteLine("1.button1_Click(): " + System.Threading.Thread.CurrentThread.ManagedThreadId);
        var t = wrap();
        Console.WriteLine("2.button1_Click(): " + System.Threading.Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine(t.Result);    
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }    
}

After several click, the form is freezing, and the output is:

1.button1_Click(): 8
1.wrap(): 8 
test(5): 13
2.wrap(): 8
2.button1_Click(): 8 
test(3): 13

To my surprise, "2.wrap():" is running in the same thread with "1.wrap():", rather than "test(5)". It seems that the code after the ConfigureAwait(false) can also jump back to UI thread.

Upvotes: 3

Views: 362

Answers (2)

Cory Nelson
Cory Nelson

Reputation: 29981

It's a deadlock -- two Tasks running in a single-threaded scheduler are waiting for each-other to complete.

Two very important things to understanding this:

  • By default, the result of an await comes back on the same scheduler as it started in.
  • The UI runs on a single-threaded event loop, and the immediate scheduler available to await in UI calls is one which dispatches onto this event loop. Only a single Task can be executing at once in this scheduler.

Because of these, you need to be especially careful about how you call blocking methods on a Task which executes inside of the UI's scheduler.

In your second example, the call to Result is waiting for the continuation setup in wrap to complete. The continuation is scheduled to be run on the UI thread, which the call to Result just happens to be blocking, so neither will ever complete.

Upvotes: 4

Stephen Cleary
Stephen Cleary

Reputation: 456507

You are causing a deadlock which I explain in detail on my blog. In summary, the await keyword will (by default) capture the current context before the await and resume the rest of the async method within that context. In this case, that "context" is the UI context, which executes code on the UI thread. So, when your code calls Result, it is blocking the UI thread, and when the await completes, it can't execute the rest of wrap on the UI thread. Thus, deadlock.

The best way to avoid this deadlock is to use await instead of Result or Wait for asynchronous tasks. I.e., change your click method to:

private async void button1_Click(object sender, EventArgs e)
{
  Task<int> t = wrap();
  Console.WriteLine(await t);
}

Upvotes: 3

Related Questions