如何在 C# 中使用 ValueTask

异步编程 相信大家已经使用很多年了,尤其在 C# 5.0 中引入的 await,async 关键词让代码编写的更加简单粗暴,你可以利用 异步编程 来提高应用程序的 响应速度吞吐率

异步方法中推荐的做法是返回 Task,如果异步方法中需要返回一些数据的话,可以将 Task 改成 Task<T>,还有一种情况是你的异步方法什么都不需要返回,那就改成 void 就可以了。

在 C# 7.0 之前,异步方法的返回值有以下三种。

  • Task

  • Task<T>

  • void

在 C# 7.0 之后,除了上面三种还可以返回 ValueTaskValueTask<T>,这些类都是在 System.Threading.Tasks.Extensions 命名空间下,这篇文章我准备和大家一起讨论下 ValueTask。

为什么要使用 ValueTask

Task 可用来表示操作的状态,什么意思呢?比如说:这个操作是否完成?是否被取消 等等,同时 异步方法 中可以返回 Task 或者 ValueTask,这里有个潜在的问题不知道大家是否注意到,因为Task 是一个引用类型,如下代码:


    public class Task : IAsyncResult, IDisposable
    {
    }

这就意味着每次调用异步方法都会在 托管堆 上生成一个 Task 实例,如果瞬间调用 1w 次,那么托管堆上也瞬间存在 1w 个 Task 实例,那这有什么问题呢? 问题在于有些场景下,你的异步方法是可以直接返回数据的,或者说可以完全同步的,比如:你的异步方法仅仅是从缓存中取数据,这时候无谓的给 GC 增加回收压力就不那么优美了。


        static Task<int> GetCustomerCountAsyc(string key)
        {
            return Task.FromResult<int>(NativeCache[key]);
        }

这时候就需要用 ValueTask 了,它提供了如下两点好处:

  • ValueTask 是值类型,所以避免了在 托管堆 上的内存分配,便拥有了更高的性能。

  • ValueTask 在实现上更加便捷 而且 灵活性更强。

总的来说: 如果你的异步方法可以直接获取结果,那么建议将 Task<T> 改成 ValueTask<T>,从而避免不必要的性能开销,TaskValueTask 都是表示 可等待 的操作,这里要注意的是,千万不要阻塞 ValueTask,毕竟它是值类型,如果真要这么做的话,调用 ValueTask.AsTask 方法将 ValueTask 转成 Task,然后在这个引用的 Task 上进行阻塞,还有一点要注意的是,ValueTask 只能被 await 一次,如果要破除这个限制的话,还是调用 AsTask 方法 将 ValueTask 转成 Task 即可。

ValueTask 的例子

假设你有一个异步方法,需要返回 Task,你可以利用 Task.FromResult 去生成一个 Task 对象,如下代码所示:


public Task<int> GetCustomerIdAsync()
{
    return Task.FromResult(1);
}

上面的代码不会在 IL 层面生成完整的 状态机代码,而仅仅是在 托管堆 中生成一个 Task 对象,要避免这种无谓的Task分配,可以用 ValueTask 撸掉它,如下代码所示:


public ValueTask<int> GetCustomerIdAsync()
{
    return new ValueTask(1);
}

接下来定义一个 IRepository 接口,新增一个返回值为 ValueTask<int> 的同步方法,如下代码所示:


    public interface IRepository<T>
    {
        ValueTask<T> GetData();
    }

从 IRepository 接口上派生一个 Repository 类,如下代码所示:


    public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
    }

最后在 Main 中来调用 GetData 方法。


        static void Main(string[] args)
        {
            IRepository<int> repository = new Repository<int>();
            var result = repository.GetData();
            if(result.IsCompleted)
                 Console.WriteLine("Operation complete...");
            else
                Console.WriteLine("Operation incomplete...");
            Console.ReadKey();
        }

现在在 IRepository 接口中添加一个异步方法 GetDataAsync,修改后的代码如下:


    public interface IRepository<T>
    {
        ValueTask<T> GetData();
        ValueTask<T> GetDataAsync();
    }

    public class Repository<T> : IRepository<T>
    {
        public ValueTask<T> GetData()
        {
            var value = default(T);
            return new ValueTask<T>(value);
        }
        public async ValueTask<T> GetDataAsync()
        {
            var value = default(T);
            await Task.Delay(100);
            return value;
        }
    }

什么时候应该使用 ValueTask

尽管 ValueTask 提供了很多好处,但用 ValueTask 替换掉 Task 也必须再三权衡,因为 ValueTask 是一个包含两个字段的值类型,而 Task 仅仅是一个字段的引用类型,这就意味着从方法返回 ValueTask 会有两个字段的开销,同时在 await 场景下生成的 异步状态机 需要更大的空间来存储 两个字段的 ValueTask 类型。

再扩展的话,如果异步方法的调用者使用 Task.WhenAll 或者 Task.WhenAny 时,而这个异步方法返回值是 ValueTask 的话开销通常会更大,为什么这么说呢? 因为你必须要将 ValueTask 转成 Task 才能在 WhenXXX 中使用,这个过程中就造成了托管堆的内存分配,如果想优化的话,可以在第一次使用 Task 的时候将这个实例缓存起来,供后续再复用。

最后总结一些 经验法则 吧。

  • 如果你的异步方法不是可立即完成的,请用 Task。

  • 如果你的异步方法是可立即完成的,比如纯内存操作,读缓存,请用 ValueTask。

不管怎样,在使用 ValueTask 之前一定要做好必要的性能分析,给自己充足的理由使用 ValueTask。

译文链接:https://www.infoworld.com/article/3565433/how-to-use-valuetask-in-csharp.html

更多高质量干货:参见我的 GitHub: csharptranslate

猜你喜欢

转载自blog.csdn.net/huangxinchen520/article/details/113034322