为什么不任务<>.在这种情况下,结果工作

本文关键字:结果 工作 这种情况下 任务 为什么不 | 更新日期: 2023-09-27 18:28:47

使用以下代码可以正常工作:

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,它会按预期工作。

更新1:这两个答案都是正确的,但我只能标记一个作为答案。基于对这个问题的理解,我做了进一步的测试。我在包装中使用了配置等待方法来让延续在 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;
}

当我第一次单击按钮时,它正在工作,但在第二次单击时再次死锁。如果我在测试(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 线程上运行。我不明白为什么第二个配置等待(假(是必要的。

更新2:

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 线程。

为什么不任务<>.在这种情况下,结果工作

这是一个

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

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

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

因此,您需要特别小心如何在 UI 的计划程序内执行的Task上调用阻止方法。

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

你造成了一个僵局,我在我的博客上详细解释了。总之,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);
}