为什么没有Task <tresult> .Result在这种情况下工作?</tresult>

时间:2013-08-24 03:56:47

标签: c# asynchronous

使用以下代码正常工作:

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;
    });
}

但是如果我使用异步方法包装测试方法,它就不起作用:

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);
}

表格失去回应。如果我使用await,它会按预期工作。

UPDATE1: 这两个答案都是正确的,但我只能将其标记为答案。基于对这个问题的理解,我做了进一步的测试。我在wrap方法中使用了ConfigureAwait 让延续在UI以外的线程中运行:

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

工作正常。然后我测试了这个:

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

第一次单击按钮时它正在工作,但第二次点击时再次出现死锁。如果我在test(3)之后添加了ConfigureAwait(false),就像这样:

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

它再次起作用,但这对我没有意义。由于第一个ConfigureAwait(false),wrap()中的所有以下同步部分都应该在非UI线程上运行。我不明白为什么第二个ConfigureAwait(false)是必要的。

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);
    }    
}

几次单击后,表单冻结,输出为:

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

令我惊讶的是,“2.wrap():”与“1.wrap():”在同一个线程中运行,而不是“test(5)”。似乎ConfigureAwait(false)之后的代码也可以跳回UI线程。

2 个答案:

答案 0 :(得分:4)

这是一个死锁 - 在单线程调度程序中运行的两个Task正在等待彼此完成。

理解这一点有两件非常重要的事情:

  • 默认情况下,await的结果会在与开始时相同的调度程序中返回。
  • UI在单线程事件循环上运行,UI调用中await可用的直接调度程序是调度到此事件循环的调度程序。在此调度程序中,一次只能执行一个任务。

由于这些原因,您需要特别小心如何调用在UI调度程序内部执行的Task上的阻塞方法。

在您的第二个示例中,对Result的调用正在等待wrap中的继续设置完成。延续计划在UI线程上运行,对Result的调用恰好阻塞,因此都不会完成。

答案 1 :(得分:3)

你造成我explain in detail on my blog的僵局。总之,await关键字将(默认情况下)捕获await之前的当前上下文,并在该上下文中恢复其余async方法。在这种情况下,“上下文”是UI上下文,它在UI线程上执行代码。因此,当您的代码调用Result时,它会阻止UI线程,并且当await完成时,它无法在UI线程上执行wrap的其余部分。因此,僵局。

避免此死锁的最佳方法是使用await代替ResultWait进行异步任务。即,将您的点击方法更改为:

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