![](/img/trans.png)
[英]Should I return a ValueTask or Task in a method which calls an async method
[英]ValueTask method with unavoidable async await calls?
我目前有以下异步方法:
private SomeObject _someObject = null;
public async Task<SomeObject> GetObjectAsync()
{
await sslim.WaitAsync();
if (_someObject == null)
{
_someObject = await InitializeSomeObjectAsync(); //starts calls to alot of async methods
}
sslim.Release();
return _someObject;
}
如果上面的代码是热路径并且调用了很多次,更改为使用ValueTask
是否安全/可以?
private SomeObject _someObject = null;
public async ValueTask<SomeObject> GetObjectAsync()
{
await sslim.WaitAsync();
if (_someObject == null)
{
_someObject = await InitializeSomeObjectAsync(); //starts calls to a lot of async methods
}
sslim.Release();
return _someObject;
}
我不确定的是sslim.WaitAsync
锁定调用,它总是会导致代码路径永远不会完全同步(即使_someObject
已经初始化),这与将ValueTask
用于可能同步执行的路径相反吗?
另一个想法,也许也将SemaphoreSlim
调用更改为同步版本会有意义吗?
private SomeObject _someObject = null;
public async ValueTask<SomeObject> GetObjectAsync()
{
sslim.Wait();
if (_someObject == null)
{
_someObject = await InitializeSomeObjectAsync(); //starts calls to a lot of async methods
}
sslim.Release();
return _someObject;
}
我计划对上述变体执行一些基准测试,但只是想从更了解哪种选项的人那里得到一些反馈。
我不确定的是 sslim.WaitAsync 锁定调用,它总是会导致代码路径永远不会完全同步
我不确定为什么会这样。 异步方法可能会同步运行,我希望SemaphoreSlim.WaitAsync
能够同步获取可用的信号量。
这与将 ValueTask 用于可能同步执行的路径相反?
即使它异步完成,使用 ValueTask<T>
也可以让您的代码避免为每次调用分配Task<T>
。如果它同步完成,它甚至会更高效,但即使它始终是异步的,您也会获得一些效率优势。 (看评论)
如果它异步完成, ValueTask<T>
将不得不进行一些分配。 如果您选择加入( DO.NET_SYSTEM_THREADING_POOLASYNCVALUETASKS
on .NET 5或[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
on .NET 6 ),则可以合并这些分配。
我目前有以下异步方法
您可能对AsyncLazy<T>
(带有AsyncLazyFlags.RetryOnFailure
)感兴趣。 它使用Task<T>
,但一旦初始化成功完成,它就是免费分配的(总是返回相同的Task<T>
实例)。
我做了一个 DIY 基准来衡量从Task<T>
切换到ValueTask<T>
对性能和分配的影响。 作为起点,我使用了以下方法:
async Task<object> TaskOne()
{
await Task.Yield();
return new object();
}
我在一个紧密的循环中连续调用并等待这个方法一秒钟,然后测量发生了多少次循环,以及总共分配了多少字节。 然后我对结果为ValueTask<object>
的变体做了同样的事情,最后我省略了await Task.Yield();
来自两个变体的线,以查看同步完成将如何影响测量。 这是完整的基准:
using System;
using System.Threading;
using System.Threading.Tasks;
public static class Program
{
static async Task Main()
{
await TestAsync("Using Task<object>", true, TaskLoop);
await TestAsync("Using ValueTask<object>", true, ValueTaskLoop);
await TestAsync("Using Task<object>", false, TaskLoop);
await TestAsync("Using ValueTask<object>", false, ValueTaskLoop);
}
static async Task TestAsync(string title, bool asynchronous,
Func<bool, CancellationToken, Task<int>> loop)
{
GC.Collect();
long mem0 = GC.GetTotalAllocatedBytes(true);
var cts = new CancellationTokenSource(1000);
int count = await loop(asynchronous, cts.Token);
long mem1 = GC.GetTotalAllocatedBytes(true);
Console.WriteLine($"{title} - " +
(asynchronous ? "Asynchronous" : "Synchronous") + " completion");
Console.WriteLine($"- Loops: {count:#,0}");
Console.WriteLine($"- Allocations: {mem1 - mem0:#,0} bytes");
double perLoop = (mem1 - mem0) / (double)count;
Console.WriteLine($"- Allocations per loop: {perLoop:#,0} bytes");
Console.WriteLine();
}
static async Task<object> TaskOne(bool asynchronous)
{
if (asynchronous) await Task.Yield();
return new object();
}
static async ValueTask<object> ValueTaskOne(bool asynchronous)
{
if (asynchronous) await Task.Yield();
return new object();
}
static async Task<int> TaskLoop(bool asynchronous, CancellationToken token)
{
int count = 0;
while (!token.IsCancellationRequested)
{
var result = await TaskOne(asynchronous);
count++;
if (result == null) break; // Make sure that the result is not optimized out
}
return count;
}
static async Task<int> ValueTaskLoop(bool asynchronous, CancellationToken token)
{
int count = 0;
while (!token.IsCancellationRequested)
{
var result = await ValueTaskOne(asynchronous);
count++;
if (result == null) break; // Make sure that the result is not optimized out
}
return count;
}
}
我在我的 PC 上得到了这些结果(.NET 5、C# 9,发布版本,未附加调试器):
Using Task<object> - Asynchronous completion
- Loops: 448,628
- Allocations: 61,034,784 bytes
- Allocations per loop: 136 bytes
Using ValueTask<object> - Asynchronous completion
- Loops: 416,055
- Allocations: 59,919,520 bytes
- Allocations per loop: 144 bytes
Using Task<object> - Synchronous completion
- Loops: 8,450,945
- Allocations: 811,290,792 bytes
- Allocations per loop: 96 bytes
Using ValueTask<object> - Synchronous completion
- Loops: 8,806,701
- Allocations: 211,360,896 bytes
- Allocations per loop: 24 bytes
我在 Fiddle 服务器上得到的结果有点不同。 它可能在调试版本上运行:
Using Task<object> - Asynchronous completion
- Loops: 667,918
- Allocations: 106,889,024 bytes
- Allocations per loop: 160 bytes
Using ValueTask<object> - Asynchronous completion
- Loops: 637,380
- Allocations: 107,084,176 bytes
- Allocations per loop: 168 bytes
Using Task<object> - Synchronous completion
- Loops: 10,128,652
- Allocations: 1,377,497,176 bytes
- Allocations per loop: 136 bytes
Using ValueTask<object> - Synchronous completion
- Loops: 9,850,096
- Allocations: 709,207,232 bytes
- Allocations per loop: 72 bytes
我的结论是,当大多数调用返回已完成的任务时,从Task<T>
切换到ValueTask<T>
是非常有利的,而如果所有调用都返回未完成的任务,则有点不利。 对于您的特定用例(保护缓存值的初始化),我认为值得进行切换,但不要期望从中获得巨大的性能提升。 可能有更好的方法来改进您的缓存机制,它们不仅可以提供更好的性能,还可以减少大量使用时的争用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.