await 与 Unwrap()

问题描述 投票:0回答:4

给出诸如

之类的方法
public async Task<Task> ActionAsync()
{
    ...
}

有什么区别

await await ActionAsync();

await ActionAsync().Unwrap();

如果有的话。

c# async-await
4个回答
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         ║
╚══════════════════╩═══════════════════════╩═════════════════╝

14
投票

不会有任何功能差异。


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 的执行速度比 .Net Core 3/.Net 5/.Net 6 的代码慢约 60%,消耗的内存多约 30%。它也出现在 Gen1 中,因此垃圾收集器在整个框架上承受的压力更高。不幸的是,BenchmarkDotNet 最高支持 .NET Framework 4.6.1,因此我无法将我的发现与 .Net Framework 4.5.1 进行比较。

结论: 如果您希望最大限度地减少内存占用,因为这对您的情况至关重要,您可能需要使用等待等待。在任何其他情况下,async-Unwrap 都会获胜,因为代码更明确且更易于阅读。 (较新版本的 .NET 执行速度更快且内存效率更高。)


0
投票

关于 Unwrap 需要创建内部任务实例才能返回的说法,我尝试了以下代码(.Net 6),似乎 Unwrap 能够在创建内部任务实例之前返回。

有人知道这是否是一个存在但在 .Net 6 中被删除的限制吗?

var nestedTask = Task.Run<Task>(() =>
{
    Task.Delay(TimeSpan.FromSeconds(10)).Wait();

    var innerTask = Task.Run(() =>
    {
        Task.Delay(TimeSpan.FromSeconds(10)).Wait();

        Console.WriteLine("Inner task about to complete...");
    });

    Console.WriteLine("Outer task about to complete...");

    return innerTask;
});

var innerTask = nestedTask.Unwrap();

Console.WriteLine("Inner task obtained");

innerTask.Wait();

Console.WriteLine("Inner task completed");

/* output:

   Inner task obtained
   Outer task about to complete...
   Inner task about to complete...
   Inner task completed

 */
© www.soinside.com 2019 - 2024. All rights reserved.