在 C# 中利用 ValueTask 避免从异步方法返回 Task 对象时分配

翻译自 Joydip Kanjilal 2020年7月6日 的文章 《How to use ValueTask in C#》

异步编程已经使用了相当长一段时间了。近年来,随着 asyncawait 关键字的引入,它变得更加强大。您可以利用异步编程来提高应用程序的响应能力和吞吐量。

C# 中异步方法的推荐返回类型是 Task。如果您想编写一个有返回值的异步方法,那么应该返回 Task<T>; 如果想编写事件处理程序,则可以返回 void。在 C# 7.0 之前,异步方法可以返回 TaskTask<T>void。从 C# 7.0 开始,异步方法还可以返回 ValueTask(作为 System.Threading.Tasks.Extensions 包的一部分可用)或 ValueTask<T>。本文就讨论一下如何在 C# 中使用 ValueTask

要使用本文提供的代码示例,您的系统中需要安装 Visual Studio 2019。如果还没有安装,您可以在这里下载 Visual Studio 2019

在 Visual Studio 中创建一个 .NET Core 控制台应用程序项目

首先,让我们在 Visual Studio 中创建一个 .NET Core 控制台应用程序项目。假设您的系统中安装了 Visual Studio 2019,请按照下面描述的步骤在 Visual Studio 中创建一个新的 .NET Core 控制台应用程序项目。

  1. 启动 Visual Studio IDE。
  2. 点击 “创建新项目”。
  3. 在 “创建新项目” 窗口中,从显示的模板列表中选择 “控制台应用(.NET Core)”。
  4. 点击 “下一步”。
  5. 在接下来显示的 “配置新项目” 窗口,指定新项目的名称和位置。
  6. 点击 “创建”。

这将在 Visual Studio 2019 中创建一个新的 .NET Core 控制台应用程序项目。我们将在本文后面的部分中使用这个项目来说明 ValueTask 的用法。

为什么要使用 ValueTask ?

Task 表示某个操作的状态,即此操作是否完成、取消等。异步方法可以返回 Task 或者 ValueTask

现在,由于 Task 是一个引用类型,从异步方法返回一个 Task 对象意味着每次调用该方法时都会在托管堆(managed heap)上分配该对象。因此,在使用 Task 时需要注意的一点是,每次从方法返回 Task 对象时都需要在托管堆中分配内存。如果你的方法执行的操作的结果立即可用或同步完成,则不需要这种分配,因此代价很高。

这正是 ValueTask 要出手相助的目的,ValueTask<T> 提供了两个主要好处。首先,ValueTask<T> 提高了性能,因为它不需要在堆(heap)中分配; 其次,它的实现既简单又灵活。当结果立即可用时,通过从异步方法返回 ValueTask<T> 代替 Task<T>,你可以避免不必要的分配开销,因为这里的 “T” 表示一个结构,而 C# 中的结构体(struct)是一个值类型(与 Task<T> 中表示类的 “T” 不同)。

C# 中 TaskValueTask 表示两种主要的 “可等待(awaitable)” 类型。请注意,您不能阻塞(block)一个 ValueTask。如果需要阻塞,则应使用 AsTask 方法将 ValueTask 转换为 Task,然后在该引用 Task 对象上进行阻塞。

另外请注意,每个 ValueTask 只能被消费(consumed)一次。这里的单词 “消费(consume)” 是指 ValueTask 可以异步等待(await)操作完成,或者利用 AsTaskValueTask 转换为 Task。但是,ValueTask 只应被消费(consumed)一次,之后 ValueTask<T> 应被忽略。

C# 中的 ValueTask 示例

假设有一个异步方法返回一个 Task。你可以利用 Task.FromResult 创建 Task 对象,如下面给出的代码片段所示。

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

上面的代码片段并没有创建整个异步状态机制,但它在托管堆(managed heap)中分配了一个 Task 对象。为了避免这种分配,您可能希望利用 ValueTask 代替,像下面给出的代码片段所示的那样。

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

下面的代码片段演示了 ValueTask 的同步实现。

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

Repository 类扩展了 IRepository 接口,并实现了如下所示的方法。

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

现在让我们将另一个方法添加到我们的存储库(repository)中,这次是一个名为 GetDataAsync 的异步方法。以下是修改后的 IRepository 接口的样子。

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

GetDataAsync 方法由 Repository 类实现,如下面给出的代码片段所示。

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

C# 中应该在什么时候使用 ValueTask ?

尽管 ValueTask 提供了一些好处,但是使用 ValueTask 代替 Task 有一定的权衡。ValueTask 是具有两个字段的值类型,而 Task 是具有单个字段的引用类型。因此,使用 ValueTask 意味着要处理更多的数据,因为方法调用将返回两个数据字段而不是一个。另外,如果您等待(await)一个返回 ValueTask 的方法,那么该异步方法的状态机也会更大,因为它必须容纳一个包含两个字段的结构体而不是在使用 Task 时的单个引用。

此外,如果异步方法的使用者使用 Task.WhenAll 或者 Task.WhenAny,在异步方法中使用 ValueTask<T> 作为返回类型可能会代价很高。这是因为您需要使用 AsTask 方法将 ValueTask<T> 转换为 Task<T>,这会引发一个分配,而如果使用起初缓存的 Task<T>,则可以轻松避免这种分配。

经验法则是这样的:当您有一段代码总是异步的时,即当操作(总是)不能立即完成时,请使用 Task。当异步操作的结果已经可用时,或者当您已经缓存了结果时,请利用 ValueTask。不管怎样,在考虑使用 ValueTask 之前,您都应该执行必要的性能分析。

ValueTaskreadonly struct 类型,Taskclass 类型。

相关链接:C# 中 Struct 和 Class 的区别总结

作者 : Joydip Kanjilal

译者 : 技术译民

出品 : 技术译站

链接 : 英文原文

如何使用 C# 中的 ValueTask的更多相关文章

  1. 理解C#中的ValueTask

    原文:https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/ 作者:Ste ...

  2. [周译见] C# 7 中的模范和实践

    原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7 关键点 遵循 .NET Framework 设计指南,时至今日,仍像十年 ...

  3. [No0000159]C# 7 中的模范和实践

    关键点 遵循 .NET Framework 设计指南,时至今日,仍像十年前首次出版一样适用. API 设计至关重要,设计不当的API大大增加错误,同时降低可重用性. 始终保持"成功之道&qu ...

  4. 深入理解 ValueTask

    深入理解 ValueTask .NET Framework 4 里面的命名空间为 System.Threading.Tasks的 Task 类.这个类以及它派生的 Task<TResult> ...

  5. 【5min+】 秋名山的竞速。 ValueTask 和 Task

    系列介绍 简介 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的. ...

  6. 【C# Task】 ValueTask/Task<TResult>

    概要 1.如果异步方法的使用者使用 Task.WhenAll 或 Task.WhenAny,则在异步方法中使用 ValueTask<T> 作为返回类型可能会产生高昂的成本.这是因为您需要使 ...

  7. C#7模范和实践

    C# 7 中的模范和实践   原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7 关键点 遵循 .NET Framework ...

  8. 【5min+】你怎么穿着品如的衣服?IEnumerable AND IEnumerator

    系列介绍 简介 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的. ...

  9. Python开源框架

    info:更多Django信息url:https://www.oschina.net/p/djangodetail: Django 是 Python 编程语言驱动的一个开源模型-视图-控制器(MVC) ...

随机推荐

  1. springMVC入门(七)------RESTFul风格的支持

    简介 RESTful风格(Representational State Transfer),又叫表现层状态转移,是一种开发理念,也是对HTTP协议很好的诠释 主要理念是将互联网中的网页.数据.服务都视 ...

  2. nova 通过 python curl 创建虚拟机---keystone v2

    #! /bin/python #coding=utf- import urllib2 import json import requests # token post_url = 'http://12 ...

  3. Hadoop的SecondaryNameNode的作用是什么?

    为节省篇幅,将SecondaryNameNode简称SNN,NameNode简称NN. NN与fsimage.edits文件 NN负责管理HDFS中所有的元数据,包括但不限于文件/目录结构.文件权限. ...

  4. maven文件合集

    maven项目目录结构 聚合项目的pom.xml <?xml version="1.0" encoding="UTF-8"?> <projec ...

  5. Redis高可用——副本机制

    目录 概念 配置 同步方式 起点 主从握手 部分同步 完全同步 执行完全同步判断条件 完全同步代码实现 为实现Redis服务的高可用,Redis官方为我们提供了副本机制(或称主从复制)和哨兵机制.副本 ...

  6. C++入门记-大纲

    缘来 由于某个不可告人的目标以及想趁着还有精力的年龄,开始了C++学习之路.C++ 诞生很多年了,但依然具有强大的生命力. 我们来看下2020年8月TIOBE 8 月编程语言排行榜. 可以看到C以及C ...

  7. 深入理解SVM,软间隔与对偶问题

    今天是机器学习专题的第33篇文章,我们继续来聊聊SVM模型. 在上一篇文章当中我们推到了SVM模型在线性可分的问题中的公式推导,我们最后得到的结论是一个带有不等式的二次项: \[\left\{\beg ...

  8. 白嫖码云Pages,两分钟的事,就能搭个百度能搜到的个人博客平台

    为了攒点钱让女儿做个富二代(笑),我就没掏钱买服务器,白嫖 GitHub Pages 搭了一个博客平台.不过遗憾的是,GitHub Pages 只能被谷歌收录,无法被百度收录,这就白白损失了一大波流量 ...

  9. oracle修改管理员密码

    运行到C盘根目录 2.输入:SET ORACLE_SID = 你的SID名称 3.输入:sqlplus/nolog 4.输入:connect/as sysdba 5.输入:altre user ris ...

  10. sql server 2008 merge matched判定条件

    SQL Server 2008 开始支持 MERGE语句    -- 源表 CREATE TABLE test_from (id INT, val VARCHAR(20));   -- 目标表 CRE ...