假设有这样一个方法:
public async Task<Task> ActionAsync()
{
...
}
什么是...的区别?
await await ActionAsync();
和
await ActionAsync().Unwrap();
如果有任何的话。
假设有这样一个方法:
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 ║
╚══════════════════╩═══════════════════════╩═════════════════╝
功能上不会有任何区别。
await
会解开一个任务,这就是它的作用。根据定义,它们是相等的。 - ServyUnwrap
返回一个新的任务,代表一个 Task<Task>
的内部任务。它能够立即返回这样一个任务;它不会阻塞操作。如果其中任何一个返回的任务不相同,那么就是两个进程中的其中一个存在错误。 - ServyContinueWIth
时,它经常出现,因为它没有假设继续操作本身是异步的重载,所以您被迫解包它。同样在其他具有相同问题的操作中也会出现,即它们给定一个操作,并且不考虑它可能是异步的情况。WhenAny
也是如此,它根据设计提供了一个任务列表。 - Servy我运行了上面的未修改示例,并得出了一个不同的结果(两个结果都为真,因此它们是等效的。我在.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的性能要慢约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也可以更快,更节省内存。)
Unwrap
仅在您调用Unwrap
时外部任务已经完成时才会返回对内部任务的引用,这是一种优化。如果外部任务尚未完成,则无法返回相等任务的引用(因为该任务可能尚不存在)。当然,您真的不应该编写关心任务引用的代码,只需确保两个任务具有相同的结果并在(大约)相同的时间内完成即可。 - Servy