首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >使用不可避免的异步等待调用的ValueTask方法?

使用不可避免的异步等待调用的ValueTask方法?
EN

Stack Overflow用户
提问于 2021-08-01 05:11:43
回答 1查看 190关注 0票数 1

我目前有以下异步方法:

代码语言:javascript
复制
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安全吗?

代码语言:javascript
复制
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调用更改为同步版本也会有意义?

代码语言:javascript
复制
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;
}

我计划对上述变化进行一些基准测试,但只是想从更有知识的人那里获得一些反馈,看看哪种选择更值得考虑。

EN

Stack Overflow用户

发布于 2021-08-26 06:46:47

我做了一个DIY基准测试来衡量从Task<T>切换到ValueTask<T>的性能和分配的影响。作为起点,我使用了下面的方法:

代码语言:javascript
复制
async Task<object> TaskOne()
{
    await Task.Yield();
    return new object();
}

我在一个紧凑的循环中连续调用和等待这个方法一秒钟,然后测量发生了多少个循环,总共分配了多少字节。然后,我对一个结果为ValueTask<object>的变量执行了同样的操作,最后,我省略了两个变量中的await Task.Yield();行,以查看同步完成将如何影响度量。以下是完整的基准测试:

代码语言:javascript
复制
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

我在我的PC上得到了这些结果(.NET 5,C# 9,Release build,没有附加调试器):

代码语言:javascript
复制
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服务器上得到的结果有点不同。它可能在Debug版本上运行:

代码语言:javascript
复制
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>是非常有利的,而如果所有调用都返回未完成的任务,则略有不利。对于您的特定用例(保护缓存值的初始化),我认为进行转换是值得的,但不要期望从这一点获得巨大的性能提升。可能有更好的方法来改进您的缓存机制,这些方法不仅可以提供更好的性能,还可以在大量使用的情况下减少争用。

票数 0
EN
查看全部 1 条回答
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/68607673

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档