等待 await 和 Unwrap() 的区别

45

假设有这样一个方法:

public async Task<Task> ActionAsync()
{
    ...
}

什么是...的区别?

await await ActionAsync();

await ActionAsync().Unwrap();

如果有任何的话。


1
在这两者之间,“await ActionAsync().Unwrap();” 明显更易读。这大概就是它们的区别所在了。 - Drew Kennedy
1
给我的回答添加基准测试。 - Leonid Vasilev
1
@DrewKennedy 我不确定它是否是,一旦你克服了双重等待的初始困惑,它就像一个特殊的关键字,表示特定的事情。 - Mr. Boy
3个回答

77

Unwrap() 创建一个代表整个操作的新任务实例,每次调用都会创建一个新的任务。与使用 await 创建的任务不同,以这种方式创建的任务与原始内部任务不同。请参阅 Unwrap() 文档,并考虑以下代码:

private async static Task Foo()
{
    Task<Task<int>> barMarker = Bar();

    Task<int> awaitedMarker = await barMarker;
    Task<int> unwrappedMarker = barMarker.Unwrap();

    Console.WriteLine(Object.ReferenceEquals(originalMarker, awaitedMarker));
    Console.WriteLine(Object.ReferenceEquals(originalMarker, unwrappedMarker));
}

private static Task<int> originalMarker;
private static Task<Task<int>> Bar()
{
    originalMarker = Task.Run(() => 1);;
    return originalMarker.ContinueWith((m) => m);
}

输出为:

True
False

使用.NET 4.5.1基准测试更新: 我测试了两个版本,结果表明双await的版本在内存使用方面更好。我使用Visual Studio 2013内存分析器进行测试,测试包括每个版本100000次调用。

x64:

╔══════════════════╦═══════════════════════╦═════════════════╗
║ Version          ║ Inclusive Allocations ║ Inclusive Bytes ║
╠══════════════════╬═══════════════════════╬═════════════════╣
║ await await      ║ 761                   ║ 30568           ║
║ await + Unwrap() ║ 100633                ║ 8025408         ║
╚══════════════════╩═══════════════════════╩═════════════════╝

x86:

╔══════════════════╦═══════════════════════╦═════════════════╗
║ Version          ║ Inclusive Allocations ║ Inclusive Bytes ║
╠══════════════════╬═══════════════════════╬═════════════════╣
║ await await      ║ 683                   ║ 16943           ║
║ await + Unwrap() ║ 100481                ║ 4809732         ║
╚══════════════════╩═══════════════════════╩═════════════════╝

我非常感激详细的答案、代码示例,甚至是基准测试!太棒了。 - hatcyl

13

功能上不会有任何区别。


感谢确认,我会尽快接受。同时,关于这个声明有没有参考资料? - hatcyl
3
你需要哪方面的参考资料?await会解开一个任务,这就是它的作用。根据定义,它们是相等的。 - Servy
Unwrap 返回一个新的任务,代表一个 Task<Task> 的内部任务。它能够立即返回这样一个任务;它不会阻塞操作。如果其中任何一个返回的任务不相同,那么就是两个进程中的其中一个存在错误。 - Servy
@Servy 你什么时候想要运行一个运行另一个任务的任务呢? - David Klempfner
@DavidKlempfner,当使用ContinueWIth时,它经常出现,因为它没有假设继续操作本身是异步的重载,所以您被迫解包它。同样在其他具有相同问题的操作中也会出现,即它们给定一个操作,并且不考虑它可能是异步的情况。WhenAny也是如此,它根据设计提供了一个任务列表。 - Servy

5

我运行了上面的未修改示例,并得出了一个不同的结果(两个结果都为真,因此它们是等效的。我在.NET SDK 6.0.300上进行了测试,但应该适用于所有版本)。

然后我稍微改进了代码,使用推荐的最佳异步等待实践并验证了我的发现:

public static class Program
{
    public static async Task Main()
    {
        await Run().ConfigureAwait(false);
    }

    public async static Task Run()
    {
        Task<Task<int>> barMarker = GetTaskOfTask();

        Task<int> awaitedMarker = await barMarker.ConfigureAwait(false);
        Task<int> unwrappedMarker = barMarker.Unwrap();

        Out(ReferenceEquals(_originalMarker, awaitedMarker));
        Out(ReferenceEquals(_originalMarker, unwrappedMarker));
    }

    private static Task<int> _originalMarker = Task.Run(() => 1);
    private static Task<Task<int>> GetTaskOfTask()
    {
        return _originalMarker.ContinueWith((m) => m, TaskScheduler.Default);
    }

    private static void Out(object t)
    {
        Console.WriteLine(t);
        Debug.WriteLine(t);
    }
}

输出结果为:

True
True

然后我对代码进行了基准测试:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1706 (21H2)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.300
  [Host]               : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  .NET 5.0             : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT
  .NET 6.0             : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  .NET Core 3.0        : .NET Core 3.1.25 (CoreCLR 4.700.22.21202, CoreFX 4.700.22.21303), X64 RyuJIT
  .NET Framework 4.6.1 : .NET Framework 4.8 (4.8.4510.0), X64 RyuJIT
  .NET Framework 4.7.2 : .NET Framework 4.8 (4.8.4510.0), X64 RyuJIT
  .NET Framework 4.8   : .NET Framework 4.8 (4.8.4510.0), X64 RyuJIT
  CoreRT 3.0           : .NET 6.0.0-rc.1.21420.1, X64 AOT


```
|     Method |                  Job |              Runtime |       Mean |    Error |    StdDev | Ratio | RatioSD |  Gen 0 |  Gen 1 | Allocated |
|----------- |--------------------- |--------------------- |-----------:|---------:|----------:|------:|--------:|-------:|-------:|----------:|
|AsynsUnwrap |             .NET 5.0 |             .NET 5.0 | 1,462.5 ns |  5.07 ns |   4.74 ns |  1.02 |    0.01 | 0.0458 |      - |     386 B |
|AsynsUnwrap |             .NET 6.0 |             .NET 6.0 | 1,435.2 ns |  6.71 ns |   6.27 ns |  1.00 |    0.00 | 0.0458 |      - |     385 B |
|AsynsUnwrap |        .NET Core 3.0 |        .NET Core 3.0 | 1,539.0 ns |  2.09 ns |   1.96 ns |  1.07 |    0.00 | 0.0458 |      - |     386 B |
|AsynsUnwrap | .NET Framework 4.6.1 | .NET Framework 4.6.1 | 2,286.3 ns |  5.33 ns |   4.98 ns |  1.59 |    0.01 | 0.0839 | 0.0038 |     546 B |
|AsynsUnwrap | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 2,267.3 ns |  6.66 ns |   5.90 ns |  1.58 |    0.01 | 0.0839 | 0.0038 |     546 B |
|AsynsUnwrap |   .NET Framework 4.8 |   .NET Framework 4.8 | 2,307.6 ns |  9.04 ns |   8.45 ns |  1.61 |    0.01 | 0.0839 | 0.0038 |     546 B |
|AsynsUnwrap |           CoreRT 3.0 |           CoreRT 3.0 |   413.2 ns |  3.78 ns |   3.54 ns |  0.29 |    0.00 | 0.0467 |      - |     391 B |
|            |                      |                      |            |          |           |       |         |        |        |           |
| AsyncAsync |             .NET 5.0 |             .NET 5.0 | 1,496.7 ns |  1.20 ns |   1.00 ns |  1.08 |    0.01 | 0.0381 |      - |     332 B |
| AsyncAsync |             .NET 6.0 |             .NET 6.0 | 1,391.5 ns |  8.25 ns |   7.72 ns |  1.00 |    0.00 | 0.0381 |      - |     332 B |
| AsyncAsync |        .NET Core 3.0 |        .NET Core 3.0 | 1,508.5 ns | 36.04 ns | 104.55 ns |  1.07 |    0.11 | 0.0381 |      - |     332 B |
| AsyncAsync | .NET Framework 4.6.1 | .NET Framework 4.6.1 | 2,179.8 ns | 11.64 ns |  10.89 ns |  1.57 |    0.01 | 0.0725 | 0.0038 |     483 B |
| AsyncAsync | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 2,213.6 ns |  8.31 ns |   7.37 ns |  1.59 |    0.01 | 0.0725 | 0.0038 |     483 B |
| AsyncAsync |   .NET Framework 4.8 |   .NET Framework 4.8 | 2,195.1 ns |  9.87 ns |   9.23 ns |  1.58 |    0.01 | 0.0725 | 0.0038 |     483 B |
| AsyncAsync |           CoreRT 3.0 |           CoreRT 3.0 |   380.3 ns |  4.55 ns |   4.03 ns |  0.27 |    0.00 | 0.0401 |      - |     336 B |

我的测量结果与认可答案有很大的差异:

  • async-async和async-Unwrap之间的速度差异非常小,我们谈论的是纳秒级别。
  • async-async比async-Unwrap具有更小的内存优势,但这种差异也非常小。

谈到框架,对于此代码,.Net Framework的性能要慢约60%,且消耗约30%的内存,比.Net Core 3/.Net 5/.Net 6要慢。 它还显示在Gen1中,因此垃圾收集器在完整框架上受到更高的压力。不幸的是,BenchmarkDotNet仅支持.NET Framework 4.6.1,因此我无法将我的发现与.NET Framework 4.5.1进行比较。

结论: 如果您需要最小化内存占用,因为这对于您的情况至关重要,则可能希望使用await await。在任何其他情况下,async-Unwrap胜出,因为代码更加明确,易于阅读。(较新版本的.NET也可以更快,更节省内存。)


.NET 6 是 .NET Core 6。所有低于 4.6.2 版本的 .NET Framework 已经到达了生命周期终点(超出支持期限)。任何受支持的 Windows 操作系统都不会提供低于 4.6.2 的版本。 - Panagiotis Kanavos
为了不再让开发人员感到困惑,并明确路径,微软决定将版本5及以上称为.NET $Version,而不是.NET Core $Version,因为它们都是 .Net Framework(最终版本:4.8)和 .Net Core(最终版本3)的后继者。 - codingdave
我知道他们做了什么。这已经被讨论了数十次。他们向.NET Core添加了任何缺失的API并更改了营销名称。.NET 5仍然是.NET Core 5。您不能仅将.NET 4.8版本升级到5。您无法在仅安装.NET Core 5的计算机上运行.NET 4.8应用程序。您需要迁移项目,重新编译和解决任何破坏性更改,最后部署到.NET Core。 - Panagiotis Kanavos
是的,你不能只是从4.8升级到5而没有问题。一些API已经被删除,你可能会被困在完整框架中。然而,我对这些框架进行了基准测试,我并没有建议升级。但如果你很幸运,可以升级一些改进免费获得。 - codingdave
3
请注意,Unwrap仅在您调用Unwrap时外部任务已经完成时才会返回对内部任务的引用,这是一种优化。如果外部任务尚未完成,则无法返回相等任务的引用(因为该任务可能尚不存在)。当然,您真的不应该编写关心任务引用的代码,只需确保两个任务具有相同的结果并在(大约)相同的时间内完成即可。 - Servy
你说得对。这个问题中的示例代码只处理了运行短暂且可能已经完成的任务。最好用一些运行时间稍长的任务再次进行相同的测试。 - codingdave

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接