网站建设信息稿,哪里有网站开发技术,建设信用卡官方网站,无忧自助建站自从VB/C#开始支持async/await后#xff0c;开发者一直在期待异步版本的IEnumerable。但直到C# 7和ValueTask发布前#xff0c;从性能的角度来看这一要求几乎是不可能实现的。
在老版本C#中#xff0c;开发者每次使用await时都需要进行内存分配。如果要枚举10,000个项…自从VB/C#开始支持async/await后开发者一直在期待异步版本的IEnumerable。但直到C# 7和ValueTask发布前从性能的角度来看这一要求几乎是不可能实现的。
在老版本C#中开发者每次使用await时都需要进行内存分配。如果要枚举10,000个项则需要分配10,000个Task对象。就算使用任务缓存这个数量也实在是太多了。通过使用ValueTask可以只在某些情况下分配内存此时IAsyncEnumerableT这种想法似乎也更可行了。
因此本文准备回顾一下2015年9月有关异步流的提议。
IAsyncEnumerable和IAsyncEnumerator
这一套接口是IEnumerableT的异步组件不过通过下列方式进行了相应的简化
public interface IAsyncEnumerable{public IAsyncEnumeratorGetEnumerator(); } public interface IAsyncEnumerator { public T Current { get; } public Task MoveNextAsync(); }
如上所示IEnumerator缺少Dispose或Reset方法。Reset的存在只是为了实现与COM的兼容性如果真的有多个枚举器实现了这种方法这种做法还是会让人感觉意外。Dispose大概会被取消因为很多人认为假设所有枚举器必然是的可释放的这想法本身就是错的。
仅仅让MoveNext成为异步的即可为我们带来两个收益
Taskbool的缓存要比TaskT的缓存更容易因此可减少内存分配量。
已经支持IEnumeratorT的类只需要额外添加一个方法。
如上所述“健谈”的异步库最大的问题就是内存分配。对于实现IAsyncEnumerator的类这并不一定会成为问题
假设正在对这样的异步序列进行foreach并且在内部进行了缓冲因此在99.9%的时间里一个元素都会是本地可用并且同步可用的。如果正在await的Task已经完成编译器会避免进行繁重的运算并且会无需暂停直接从任务中获得所需的值。如果调用的特定方法内所有await的Task均已完成那么所用方法绝对不会分配状态机或通过委托存储为继续Continuation因为这些东西只会在首次需要时进行构造。
就算异步方法同步达到了return语句由于await无需暂停此时依然需要构造一个Task才能返回。因此一般来说这依然需要进行一次分配。然而编译器为该过程使用的助手API实际上会对已完成的Task缓存某些通用值包括true和false。简而言之针对已缓冲序列调用的MoveNextAsync以及调用方法通常什么都不会分配。
但如果数据并不一定被缓冲此时又会怎样
猜测此时适合使用ValueTask或其他自定义的任务类型。理论上我们甚至可以提供一个“可重置的任务”借此在枚举器调用MoveNextAsync时清除“已完成”标记。此类优化尚未进行过公开的讨论甚至有可能是不可行的不过C# 7开始考虑这个问题。
异步LINQ
回到2015提议接下来要考虑的是LINQ。LINQ最大的问题在于synchronous/asynchronous源和synchronous/asynchronous委托之间组合的绝对数目。例如一个简单的Where函数可能需要四个重载Overload
public static IEnumerableWhere(this IEnumerable source, Func predicate); public static IAsyncEnumerable Where (this IAsyncEnumerable source, Func predicate); public static IAsyncEnumerable Where (this IEnumerable source, Func predicate); public static IAsyncEnumerable Where (this IAsyncEnumerable source, Func predicate);
因此提议中提到
因此我们要么需要将LINQ的外围应用翻四倍要么需要为该语言引入某种新的隐式转换例如从IEnumerable变为IAsyncEnumerable或从Func变为Func。这种做法值得考虑但我们觉得也许可以通过某种方式让LINQ支持异步序列。
翻四倍的方法的影响可能被低估了因为一些LINQ操作可能需要多个委托。
另一个问题是到底要使用基于Task还是基于ValueTask的委托。2017年的一份文档提到了这个问题“希望通过异步委托实现重载可通过ValueTask提高效率”。
语言支持
很明显对于IAsyncEnumerable人们首先会考虑异步foreach但如果代码中根本没有实际出现Task对象又会如何这方面讨论过的选项包括
foreach (string s in asyncStream) { ... } //implied await
await foreach (string s in asyncStream) { ... }
foreach (await string s in asyncStream) { ... }
foreach async (string s in asyncStream) { ... }
foreach await (string s in asyncStream) { ... }
同样的问题在于执行诸如ConfigureAwait等操作时从性能的角度考虑库中的哪些东西是最重要的如果不使用Task又该如何进行ConfigureAwait此时的最佳做法是同时向IAsyncEnumerable增加一个ConfigureAwait扩展方法。这样即可返回包装序列进而返回包装枚举器其MoveNextAsync可返回针对包装的枚举器中所包含的MoveNextAsync方法返回的任务调用ConfigureAwait后的结果
该提议进一步谈到
为此必须要让异步foreach像目前的同步foreach一样成为基于模式的Pattern based这样即可灵活调用任何GetEnumerator、MoveNext和Current成员而无须考虑对象是否实现了正式的“接口”。这样做的原因在于Task.ConfigureAwait的结果并不是Task。
继续回到我们的猜测这意味着一个类将可以在提供基于自定义枚举器的ValueTask等内容同时继续支持IAsyncEnumerableT。这一点与ListT的工作方式类似可通过通用的IEnumerableT和基于结构Struct的备用枚举器实现更高性能。
取消令牌
接着是2017年1月的会议纪要一起看看异步流。首先是一个有关取消令牌Cancellation token的棘手问题。
GetAsyncEnumerator能够接受可选的取消令牌但具体是怎样做的根据会议纪要
使用另一个重载 :-(使用一个默认参数 (CLS :-()使用一个扩展方法要求该扩展方法位于范围内
有趣的是尽管C#长期以来都支持默认参数但CLS即通用语言规范的约束依然是生效的。对于不熟悉这一概念的人可以这样理解CLS定义了.NET平台上所有语言必须支持的最小功能集。另外大部分库尤其是基础库的令牌必须兼容CLS。
抛开具体API不谈IAsyncEnumerableT获得取消令牌的方法就很明确了。但诸如Foreach block等迭代器如何获得取消令牌还不明确。他们正在考虑通过某种“走后门”的方法从状态机中得到令牌但这可能需要修改枚举器的接口。
TryMoveNext?
继续看看之前提到的性能问题如果可以在不进行异步调用的情况下检查是否已经具备可用数据情况又会如何
这正是添加bool? TryMoveNext()方法的理论依据。True/false可以按照预期工作但如果获得了空值则意味着需要调用MoveNextAsync来确定是否存在任何额外的数据。
此外也可考虑使用显式Chunking其中每个调用可返回仅代表已缓冲数据的可枚举结果。
public IAsyncEnumerable GetElementsAsync();
这些提议在供应方和消耗方目前都还存在一定的问题因此据此决策尚未确定。
原文地址http://www.infoq.com/cn/news/2017/05/async-streams .NET社区新闻深度好文微信中搜索dotNET跨平台或扫描二维码关注