Given a method such as
public async Task<Task> ActionAsync()
{
...
}
What is the difference between
await await ActionAsync();
and
await ActionAsync().Unwrap();
if any.
Given a method such as
public async Task<Task> ActionAsync()
{
...
}
What is the difference between
await await ActionAsync();
and
await ActionAsync().Unwrap();
if any.
Unwrap()
creates a new task instance that represent whole operation on each call. In contrast to await
task created in such a way is differ from original inner task. See the Unwrap() docs, and consider the following code:
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);
}
Output is:
True
False
Update with benchmark for .NET 4.5.1: I tested both versions, and it turns out that version with double await
is better in terms of memory usage.
I used Visual Studio 2013 memory profiler. Test includes 100000 calls of each version.
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 ║
╚══════════════════╩═══════════════════════╩═════════════════╝
There won't be any functional difference.
await
unwraps a task. That's what it does. They're equal by definition. –
Slowworm Unwrap
returns a new task that represents the inner task of a Task<Task>
. It will be able to return such a task virtually immediately; it will not be a blocking operation. If the tasks returned by either weren't identical, then it would be a bug in one of the two processes. –
Slowworm ContinueWIth
, as it doesn't have overloads assuming the continuation is itself asynchronous, so you're forced to unwrap it. It also comes up with other operations with the same problem, namely they are given an operation and don't consider the case it could be asynchronous. Also with WhenAny
which provides a task of tasks by design. –
Slowworm I run the unmodified sample from above and came to a different result (both are true, so they are equivalent. I tested that on .NET SDK 6.0.300 but it should hold for all).
Then I improved the code slightly to use recommended best practices for async await and verified my findings:
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);
}
}
Output is:
True
True
Then I benchmarked the code:
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 |
My measurements vary drastically from the accepted answer:
Talking about the framework .Net Framework performs around 60% slower and consumes around 30% more memory than .Net Core 3/.Net 5/.Net 6 for this code. It also shows up in Gen1, so the garbage collector is stressed higher on the full framework. Unfortunately BenchmarkDotNet supports up to .NET Framework 4.6.1, so I could not compare my findings against .Net Framework 4.5.1.
conclusion: If you're for minimizing the memory footprint because this is critical in your case you might want to use await await. In any other case async-Unwrap wins as the code is more explicit and its easier to read. (More recent versions of .NET perform faster and more memory efficient as well.)
Unwrap
will only return a reference to the inner task if the outer task has run to completion at the time you call Unwrap
, as an optimization. If the outer task isn't completed yet it couldn't possibly return a reference equal task (as that task possibly doesn't exist yet). Of course, you really shouldn't be writing code that cares about task references like that, only that the two tasks have the same result and complete at (about) the same time. –
Slowworm Regarding the claim that Unwrap requires the inner task instance to be created and the outer task to complete, so the outer task can return the inner task reference. I've tried the following code (.Net 6), and it seems Unwrap is able to return before the inner task instance is created.
Does anybody know if this is a limitation that existed but was removed in .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
*/
© 2022 - 2024 — McMap. All rights reserved.
await ActionAsync().Unwrap();
is definitely easier to read between the two. That's about where the differences end. – Gunslinger