简体   繁体   English

具有不可避免的异步等待调用的 ValueTask 方法?

[英]ValueTask method with unavoidable async await calls?

I currently have the following async method:我目前有以下异步方法:

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;
}

If the above code is a hot path and called many times, is it safe/ok to change to use ValueTask ?如果上面的代码是热路径并且调用了很多次,更改为使用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;
}

What I'm unsure about is the sslim.WaitAsync locking call, which will always cause the code path to never be completely synchronous (even if _someObject has already been initialized), which is counter to using ValueTask for paths that can possible perform synchronously?我不确定的是sslim.WaitAsync锁定调用,它总是会导致代码路径永远不会完全同步(即使_someObject已经初始化),这与将ValueTask用于可能同步执行的路径相反吗?

Another thought, maybe also changing the SemaphoreSlim call to the sync version would make sense?另一个想法,也许也将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;
}

I plan to perform some benchmarks on the above variations, but just wanted to get some feedback from people who are more knowledgeable as to which option would be good to consider.我计划对上述变体执行一些基准测试,但只是想从更了解哪种选项的人那里得到一些反馈。

What I'm unsure about is the sslim.WaitAsync locking call, which will always cause the code path to never be completely synchronous我不确定的是 sslim.WaitAsync 锁定调用,它总是会导致代码路径永远不会完全同步

I'm not sure why that would be the case.我不确定为什么会这样。 Asynchronous methods may behave synchronously, and I would expect SemaphoreSlim.WaitAsync to synchronously acquire the semaphore if it's available.异步方法可能会同步运行,我希望SemaphoreSlim.WaitAsync能够同步获取可用的信号量。

which is counter to using ValueTask for paths that can possible perform synchronously?这与将 ValueTask 用于可能同步执行的路径相反?

Even if it completes asynchronously, using ValueTask<T> allows your code to avoid an allocation of a Task<T> for each invocation.即使它异步完成,使用ValueTask<T>也可以让您的代码避免为每次调用分配Task<T> If it completes synchronously, it's even more efficient, but you'll get some efficiency benefits even if it's always asynchronous.如果它同步完成,它甚至会高效,但即使它始终是异步的,您也会获得一些效率优势。 (see comments) (看评论)

If it completes asynchronously, ValueTask<T> will have to do some allocations.如果它异步完成, ValueTask<T>将不得不进行一些分配。 These allocations can be pooled if you opt into it ( DO.NET_SYSTEM_THREADING_POOLASYNCVALUETASKS on .NET 5 or [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] on .NET 6 ).如果您选择加入( DO.NET_SYSTEM_THREADING_POOLASYNCVALUETASKS on .NET 5[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] on .NET 6 ),则可以合并这些分配。

I currently have the following async method我目前有以下异步方法

You may be interested in AsyncLazy<T> (with AsyncLazyFlags.RetryOnFailure ) .您可能对AsyncLazy<T> (带有AsyncLazyFlags.RetryOnFailure感兴趣。 It uses Task<T> , but once the initialization completes successfully, it is allocation free (always returning the same Task<T> instance).它使用Task<T> ,但一旦初始化成功完成,它就是免费分配的(总是返回相同的Task<T>实例)。

I made a DIY benchmark to measure the effect of switching from Task<T> to ValueTask<T> , regarding performance and allocations.我做了一个 DIY 基准来衡量从Task<T>切换到ValueTask<T>对性能和分配的影响。 As starting point I used the method below:作为起点,我使用了以下方法:

async Task<object> TaskOne()
{
    await Task.Yield();
    return new object();
}

I invoked and awaited this method continuously in a tight loop for one second, and then measured how many loops happened, and how many bytes were allocated in total.我在一个紧密的循环中连续调用并等待这个方法一秒钟,然后测量发生了多少次循环,以及总共分配了多少字节。 Then I did the same with a variant having ValueTask<object> as result, and finally I omitted the await Task.Yield();然后我对结果为ValueTask<object>的变体做了同样的事情,最后我省略了await Task.Yield(); line from both variants, to see how a synchronous completion would affect the measurements.来自两个变体的线,以查看同步完成将如何影响测量。 Here is the complete benchmark:这是完整的基准:

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;
    }
}

Try it on Fiddle .在 Fiddle 上试试

I got these results on my PC (.NET 5, C# 9, Release build, no debugger attached):我在我的 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

The results I got on the Fiddle server were a bit different.我在 Fiddle 服务器上得到的结果有点不同。 It is probably running on Debug build:它可能在调试版本上运行:

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

My conclusion is that switching from Task<T> to ValueTask<T> is quite advantageous when most of the invocations return completed tasks, and it is slightly disadvantageous if all of the invocations return incomplete tasks.我的结论是,当大多数调用返回已完成的任务时,从Task<T>切换到ValueTask<T>是非常有利的,而如果所有调用都返回未完成的任务,则有点不利。 For your specific use case (protecting the initialization of cached values) I think that it is worth making the switch, but don't expect massive performance gains from this.对于您的特定用例(保护缓存值的初始化),我认为值得进行切换,但不要期望从中获得巨大的性能提升。 There are probably better ways to improve your caching mechanism, that offer not only better performance, but also less contention under heavy usage.可能有更好的方法来改进您的缓存机制,它们不仅可以提供更好的性能,还可以减少大量使用时的争用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM