给出诸如
之类的方法public async Task<Task> ActionAsync()
{
...
}
有什么区别
await await ActionAsync();
和
await ActionAsync().Unwrap();
如果有的话。
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 ║
╚══════════════════╩═══════════════════════╩═════════════════╝
不会有任何功能差异。
我运行上面未经修改的示例,得到了不同的结果(两者都是正确的,因此它们是等效的。我在 .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 |
我的测量结果与接受的答案有很大不同:
谈论框架 .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 执行速度更快且内存效率更高。)
关于 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
*/